Improve Anthropic function calling

- factor out the common funciton calling logic form AnthropicChatModel to the AbstractToolCallSupport.
 - improve the AbstractToolCallSupport isToolCall to handle OpenAi and Anthropic.
 - fix an issue with the function calling streaming aggreagation leading to lost usage statistics.
 - small code improvements for OpenAiChatModel.
This commit is contained in:
Christian Tzolov
2024-07-25 18:13:22 +02:00
parent 554fbcd305
commit 6363352b82
8 changed files with 178 additions and 164 deletions

View File

@@ -15,40 +15,6 @@
*/
package org.springframework.ai.anthropic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock.ContentBlockType;
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
import org.springframework.ai.anthropic.metadata.AnthropicUsage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
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.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.ModelOptionsUtils;
import org.springframework.ai.chat.model.AbstractToolCallSupport;
import org.springframework.ai.model.function.FunctionCallbackContext;
import org.springframework.ai.retry.RetryUtils;
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.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashSet;
@@ -57,6 +23,40 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock.Type;
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
import org.springframework.ai.anthropic.metadata.AnthropicUsage;
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;
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.FunctionCallbackContext;
import org.springframework.ai.retry.RetryUtils;
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.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* The {@link ChatModel} implementation for the Anthropic service.
*
@@ -150,17 +150,17 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
ChatCompletionRequest request = createRequest(prompt, false);
return this.retryTemplate.execute(ctx -> {
ResponseEntity<ChatCompletionResponse> completionEntity = this.anthropicApi.chatCompletionEntity(request);
ResponseEntity<ChatCompletionResponse> completionEntity = this.retryTemplate
.execute(ctx -> this.anthropicApi.chatCompletionEntity(request));
if (this.isToolFunctionCall(completionEntity.getBody())) {
List<Message> toolCallMessageConversation = this.handleToolCallRequests(prompt.getInstructions(),
completionEntity.getBody());
return this.call(new Prompt(toolCallMessageConversation, prompt.getOptions()));
}
ChatResponse chatResponse = toChatResponse(completionEntity.getBody());
return toChatResponse(completionEntity.getBody());
});
if (this.isToolCall(chatResponse, Set.of("tool_use"))) {
var toolCallConversation = handleToolCalls(prompt, chatResponse);
return this.call(new Prompt(toolCallConversation, prompt.getOptions()));
}
return chatResponse;
}
@Override
@@ -168,68 +168,66 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
ChatCompletionRequest request = createRequest(prompt, true);
return this.retryTemplate.execute(ctx -> {
Flux<ChatCompletionResponse> response = this.retryTemplate
.execute(ctx -> this.anthropicApi.chatCompletionStream(request));
Flux<ChatCompletionResponse> response = this.anthropicApi.chatCompletionStream(request);
return response.switchMap(chatCompletionResponse -> {
return response.switchMap(chatCompletionResponse -> {
ChatResponse chatResponse = toChatResponse(chatCompletionResponse);
if (this.isToolFunctionCall(chatCompletionResponse)) {
List<Message> toolCallMessageConversation = this.handleToolCallRequests(prompt.getInstructions(),
chatCompletionResponse);
return this.stream(new Prompt(toolCallMessageConversation, prompt.getOptions()));
}
if (this.isToolCall(chatResponse, Set.of("tool_use"))) {
var toolCallConversation = handleToolCalls(prompt, chatResponse);
return this.stream(new Prompt(toolCallConversation, prompt.getOptions()));
}
return Mono.just(chatCompletionResponse).map(this::toChatResponse);
});
return Mono.just(chatResponse);
});
}
private List<Message> handleToolCallRequests(List<Message> previousMessages,
ChatCompletionResponse chatCompletionResponse) {
AnthropicMessage anthropicAssistantMessage = new AnthropicMessage(chatCompletionResponse.content(),
Role.ASSISTANT);
List<ContentBlock> toolToUseList = anthropicAssistantMessage.content()
.stream()
.filter(c -> c.type() == ContentBlock.ContentBlockType.TOOL_USE)
.toList();
List<AssistantMessage.ToolCall> toolCalls = new ArrayList<>();
for (ContentBlock toolToUse : toolToUseList) {
var functionCallId = toolToUse.id();
var functionName = toolToUse.name();
var functionArguments = ModelOptionsUtils.toJsonString(toolToUse.input());
toolCalls.add(new AssistantMessage.ToolCall(functionCallId, "function", functionName, functionArguments));
}
AssistantMessage assistantMessage = new AssistantMessage("", Map.of(), toolCalls);
ToolResponseMessage toolResponseMessage = this.executeFunctions(assistantMessage);
// History
List<Message> toolCallMessageConversation = new ArrayList<>(previousMessages);
toolCallMessageConversation.add(assistantMessage);
toolCallMessageConversation.add(toolResponseMessage);
return toolCallMessageConversation;
}
private ChatResponse toChatResponse(ChatCompletionResponse chatCompletion) {
if (chatCompletion == null) {
logger.warn("Null chat completion returned");
return new ChatResponse(List.of());
}
List<Generation> generations = chatCompletion.content().stream().map(content -> {
return new Generation(content.text(), Map.of())
.withGenerationMetadata(ChatGenerationMetadata.from(chatCompletion.stopReason(), null));
}).toList();
List<Generation> generations = chatCompletion.content()
.stream()
.filter(content -> content.type() != ContentBlock.Type.TOOL_USE)
.map(content -> {
new AssistantMessage(content.text(), Map.of());
return new Generation(new AssistantMessage(content.text(), Map.of()),
ChatGenerationMetadata.from(chatCompletion.stopReason(), null));
})
.toList();
return new ChatResponse(generations, from(chatCompletion));
List<Generation> allGenerations = new ArrayList<>(generations);
List<ContentBlock> toolToUseList = chatCompletion.content()
.stream()
.filter(c -> c.type() == ContentBlock.Type.TOOL_USE)
.toList();
if (!CollectionUtils.isEmpty(toolToUseList)) {
List<AssistantMessage.ToolCall> toolCalls = new ArrayList<>();
for (ContentBlock toolToUse : toolToUseList) {
var functionCallId = toolToUse.id();
var functionName = toolToUse.name();
var functionArguments = ModelOptionsUtils.toJsonString(toolToUse.input());
toolCalls
.add(new AssistantMessage.ToolCall(functionCallId, "function", functionName, functionArguments));
}
AssistantMessage assistantMessage = new AssistantMessage("", Map.of(), toolCalls);
Generation toolCallGeneration = new Generation(assistantMessage,
ChatGenerationMetadata.from(chatCompletion.stopReason(), null));
allGenerations.add(toolCallGeneration);
}
return new ChatResponse(allGenerations, this.from(chatCompletion));
}
private ChatResponseMetadata from(AnthropicApi.ChatCompletionResponse result) {
@@ -288,8 +286,8 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
}
if (!CollectionUtils.isEmpty(assistantMessage.getToolCalls())) {
for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
contentBlocks.add(new ContentBlock(ContentBlockType.TOOL_USE, toolCall.id(),
toolCall.name(), ModelOptionsUtils.jsonToMap(toolCall.arguments())));
contentBlocks.add(new ContentBlock(Type.TOOL_USE, toolCall.id(), toolCall.name(),
ModelOptionsUtils.jsonToMap(toolCall.arguments())));
}
}
return new AnthropicMessage(contentBlocks, Role.ASSISTANT);
@@ -297,7 +295,7 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
else if (message.getMessageType() == MessageType.TOOL) {
List<ContentBlock> toolResponses = ((ToolResponseMessage) message).getResponses()
.stream()
.map(toolResponse -> new ContentBlock(ContentBlockType.TOOL_RESULT, toolResponse.id(),
.map(toolResponse -> new ContentBlock(Type.TOOL_RESULT, toolResponse.id(),
toolResponse.responseData()))
.toList();
return new AnthropicMessage(toolResponses, Role.USER);
@@ -355,16 +353,6 @@ public class AnthropicChatModel extends AbstractToolCallSupport implements ChatM
}).toList();
}
@SuppressWarnings("null")
protected boolean isToolFunctionCall(ChatCompletionResponse response) {
if (response == null || CollectionUtils.isEmpty(response.content())) {
return false;
}
return response.content()
.stream()
.anyMatch(content -> content.type() == ContentBlock.ContentBlockType.TOOL_USE);
}
@Override
public ChatOptions getDefaultOptions() {
return AnthropicChatOptions.fromOptions(this.defaultOptions);

View File

@@ -23,6 +23,8 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.anthropic.api.StreamHelper.ChatCompletionResponseBuilder;
import org.springframework.ai.model.ChatModelDescription;
import org.springframework.ai.model.ModelOptionsUtils;
@@ -53,6 +55,8 @@ import reactor.core.publisher.Mono;
*/
public class AnthropicApi {
private static final Logger logger = LoggerFactory.getLogger(AnthropicApi.class);
private static final String HEADER_X_API_KEY = "x-api-key";
private static final String HEADER_ANTHROPIC_VERSION = "anthropic-version";
@@ -415,7 +419,7 @@ public class AnthropicApi {
*/
@JsonInclude(Include.NON_NULL)
public record ContentBlock( // @formatter:off
@JsonProperty("type") ContentBlockType type,
@JsonProperty("type") Type type,
@JsonProperty("source") Source source,
@JsonProperty("text") String text,
@@ -438,67 +442,77 @@ public class AnthropicApi {
}
public ContentBlock(Source source) {
this(ContentBlockType.IMAGE, source, null, null, null, null, null, null, null);
this(Type.IMAGE, source, null, null, null, null, null, null, null);
}
public ContentBlock(String text) {
this(ContentBlockType.TEXT, null, text, null, null, null, null, null, null);
this(Type.TEXT, null, text, null, null, null, null, null, null);
}
// Tool result
public ContentBlock(ContentBlockType type, String toolUseId, String content) {
public ContentBlock(Type type, String toolUseId, String content) {
this(type, null, null, null, null, null, null, toolUseId, content);
}
public ContentBlock(ContentBlockType type, Source source, String text, Integer index) {
public ContentBlock(Type type, Source source, String text, Integer index) {
this(type, source, text, index, null, null, null, null, null);
}
// Tool use input JSON delta streaming
public ContentBlock(ContentBlockType type, String id, String name, Map<String, Object> input) {
public ContentBlock(Type type, String id, String name, Map<String, Object> input) {
this(type, null, null, null, id, name, input, null, null);
}
/**
* The type of this message.
* The ContentBlock type.
*/
public enum ContentBlockType {
public enum Type {
/**
* Tool request
*/
@JsonProperty("tool_use")
TOOL_USE,
TOOL_USE("tool_use"),
/**
* Send tool result back to LLM.
*/
@JsonProperty("tool_result")
TOOL_RESULT,
TOOL_RESULT("tool_result"),
/**
* Text message.
*/
@JsonProperty("text")
TEXT,
TEXT("text"),
/**
* Text delta message. Returned from the streaming response.
*/
@JsonProperty("text_delta")
TEXT_DELTA,
TEXT_DELTA("text_delta"),
/**
* Tool use input partial JSON delta streaming.
*/
@JsonProperty("input_json_delta")
INPUT_JSON_DELTA,
INPUT_JSON_DELTA("input_json_delta"),
/**
* Image message.
*/
@JsonProperty("image")
IMAGE;
IMAGE("image");
public final String value;
Type(String value) {
this.value = value;
}
public String getValue() {
return this.value;
}
}
@@ -902,6 +916,7 @@ public class AnthropicApi {
.takeUntil(SSE_DONE_PREDICATE)
.filter(SSE_DONE_PREDICATE.negate())
.map(content -> ModelOptionsUtils.jsonToObject(content, StreamEvent.class))
.filter(event -> event.type() != EventType.PING)
// Detect if the chunk is part of a streaming function call.
.map(event -> {
if (this.streamHelper.isToolUseStart(event)) {

View File

@@ -21,7 +21,7 @@ import java.util.concurrent.atomic.AtomicReference;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock.ContentBlockType;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock.Type;
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
import org.springframework.ai.anthropic.api.AnthropicApi.Usage;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlockDeltaEvent;
@@ -55,15 +55,15 @@ public class StreamHelper {
if (event == null || event.type() == null || event.type() != EventType.CONTENT_BLOCK_START) {
return false;
}
return "tool_use".equals(((ContentBlockStartEvent) event).contentBlock().type());
return ContentBlock.Type.TOOL_USE.getValue().equals(((ContentBlockStartEvent) event).contentBlock().type());
}
public boolean isToolUseFinish(StreamEvent event) {
if (event == null || event.type() == null || event.type() != EventType.MESSAGE_DELTA) {
if (event == null || event.type() == null || event.type() != EventType.CONTENT_BLOCK_STOP) {
return false;
}
return "tool_use".equals(((MessageDeltaEvent) event).delta().stopReason());
return true;
}
public StreamEvent mergeToolUseEvents(StreamEvent previousEvent, StreamEvent event) {
@@ -73,7 +73,7 @@ public class StreamHelper {
if (event.type() == EventType.CONTENT_BLOCK_START) {
ContentBlockStartEvent contentBlockStart = (ContentBlockStartEvent) event;
if ("tool_use".equals(contentBlockStart.contentBlock().type())) {
if (ContentBlock.Type.TOOL_USE.getValue().equals(contentBlockStart.contentBlock().type())) {
ContentBlockStartEvent.ContentBlockToolUse cbToolUse = (ContentBlockToolUse) contentBlockStart
.contentBlock();
@@ -85,7 +85,7 @@ public class StreamHelper {
}
else if (event.type() == EventType.CONTENT_BLOCK_DELTA) {
ContentBlockDeltaEvent contentBolckDelta = (ContentBlockDeltaEvent) event;
if ("input_json_delta".equals(contentBolckDelta.delta().type())) {
if (ContentBlock.Type.INPUT_JSON_DELTA.getValue().equals(contentBolckDelta.delta().type())) {
return eventAggregator
.appendPartialJson(((ContentBlockDeltaJson) contentBolckDelta.delta()).partialJson());
}
@@ -96,9 +96,6 @@ public class StreamHelper {
return eventAggregator;
}
}
else if (isToolUseFinish(event)) {
return eventAggregator;
}
return event;
}
@@ -128,8 +125,7 @@ public class StreamHelper {
List<ContentBlock> content = eventToolUseBuilder.getToolContentBlocks()
.stream()
.map(tooToUse -> new ContentBlock(ContentBlockType.TOOL_USE, tooToUse.id(), tooToUse.name(),
tooToUse.input()))
.map(tooToUse -> new ContentBlock(Type.TOOL_USE, tooToUse.id(), tooToUse.name(), tooToUse.input()))
.toList();
contentBlockReference.get().withContent(content);
}
@@ -142,7 +138,7 @@ public class StreamHelper {
+ contentBlockStartEvent.contentBlock().type());
ContentBlockText contentBlockText = (ContentBlockText) contentBlockStartEvent.contentBlock();
ContentBlock contentBlock = new ContentBlock(ContentBlockType.TEXT, null, contentBlockText.text(),
ContentBlock contentBlock = new ContentBlock(Type.TEXT, null, contentBlockText.text(),
contentBlockStartEvent.index());
contentBlockReference.get().withType(event.type().name()).withContent(List.of(contentBlock));
}
@@ -156,8 +152,7 @@ public class StreamHelper {
ContentBlockDeltaText deltaTxt = (ContentBlockDeltaText) contentBlockDeltaEvent.delta();
var contentBlock = new ContentBlock(ContentBlockType.TEXT_DELTA, null, deltaTxt.text(),
contentBlockDeltaEvent.index());
var contentBlock = new ContentBlock(Type.TEXT_DELTA, null, deltaTxt.text(), contentBlockDeltaEvent.index());
contentBlockReference.get().withType(event.type().name()).withContent(List.of(contentBlock));
}
@@ -182,7 +177,6 @@ public class StreamHelper {
}
}
else if (event.type().equals(EventType.MESSAGE_STOP)) {
}
else {
contentBlockReference.get().withType(event.type().name()).withContent(List.of());

View File

@@ -29,7 +29,7 @@ import org.springframework.ai.anthropic.api.AnthropicApi;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionResponse;
import org.springframework.ai.anthropic.api.AnthropicApi.ChatCompletionRequest;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock.ContentBlockType;
import org.springframework.ai.anthropic.api.AnthropicApi.ContentBlock.Type;
import org.springframework.ai.anthropic.api.AnthropicApi.AnthropicMessage;
import org.springframework.ai.anthropic.api.AnthropicApi.Role;
import org.springframework.ai.anthropic.api.AnthropicApi.Tool;
@@ -117,7 +117,7 @@ public class AnthropicApiToolIT {
List<ContentBlock> toolToUseList = response.getBody()
.content()
.stream()
.filter(c -> c.type() == ContentBlock.ContentBlockType.TOOL_USE)
.filter(c -> c.type() == ContentBlock.Type.TOOL_USE)
.toList();
if (CollectionUtils.isEmpty(toolToUseList)) {
@@ -146,7 +146,7 @@ public class AnthropicApiToolIT {
logger.info("Function response : " + content);
toolResults.add(new ContentBlock(ContentBlockType.TOOL_RESULT, id, content));
toolResults.add(new ContentBlock(Type.TOOL_RESULT, id, content));
}
// Add function response message to the conversation history

View File

@@ -186,18 +186,6 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
return chatResponse;
}
public static ChatResponseMetadata from(OpenAiApi.ChatCompletion result, RateLimit rateLimit) {
Assert.notNull(result, "OpenAI ChatCompletionResult must not be null");
return ChatResponseMetadata.builder()
.withId(result.id())
.withUsage(OpenAiUsage.from(result.usage()))
.withModel(result.model())
.withRateLimit(rateLimit)
.withKeyValue("created", result.created())
.withKeyValue("system-fingerprint", result.systemFingerprint())
.build();
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
@@ -232,7 +220,7 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
// @formatter:on
if (chatCompletion2.usage() != null) {
return new ChatResponse(generations, from(chatCompletion2));
return new ChatResponse(generations, from(chatCompletion2, null));
}
else {
return new ChatResponse(generations);
@@ -259,18 +247,7 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
});
}
private ChatResponseMetadata from(OpenAiApi.ChatCompletion result) {
Assert.notNull(result, "OpenAI ChatCompletionResult must not be null");
return ChatResponseMetadata.builder()
.withId(result.id())
.withUsage(OpenAiUsage.from(result.usage()))
.withModel(result.model())
.withKeyValue("created", result.created())
.withKeyValue("system-fingerprint", result.systemFingerprint())
.build();
}
private static Generation buildGeneration(Choice choice, Map<String, Object> metadata) {
private Generation buildGeneration(Choice choice, Map<String, Object> metadata) {
List<AssistantMessage.ToolCall> toolCalls = choice.message().toolCalls() == null ? List.of()
: choice.message()
.toolCalls()
@@ -285,6 +262,21 @@ public class OpenAiChatModel extends AbstractToolCallSupport implements ChatMode
return new Generation(assistantMessage, generationMetadata);
}
private ChatResponseMetadata from(OpenAiApi.ChatCompletion result, RateLimit rateLimit) {
Assert.notNull(result, "OpenAI ChatCompletionResult must not be null");
var builder = ChatResponseMetadata.builder()
.withId(result.id())
.withUsage(OpenAiUsage.from(result.usage()))
.withModel(result.model())
.withRateLimit(rateLimit)
.withKeyValue("created", result.created())
.withKeyValue("system-fingerprint", result.systemFingerprint());
if (rateLimit != null) {
builder.withRateLimit(rateLimit);
}
return builder.build();
}
/**
* Convert the ChatCompletionChunk into a ChatCompletion. The Usage is set to null.
* @param chunk the ChatCompletionChunk to convert

View File

@@ -77,4 +77,10 @@ public class AssistantMessage extends AbstractMessage {
return Objects.hash(super.hashCode(), toolCalls);
}
@Override
public String toString() {
return "AssistantMessage [messageType=" + messageType + ", toolCalls=" + toolCalls + ", textContent="
+ textContent + ", metadata=" + metadata + "]";
}
}

View File

@@ -51,6 +51,12 @@ public interface ChatGenerationMetadata extends ResultMetadata {
public String getFinishReason() {
return finishReason;
}
@Override
public String toString() {
return "ChatGenerationMetadata{finishReason=" + finishReason + "," + "contentFilterMetadata="
+ contentFilterMetadata + "}";
}
};
}

View File

@@ -19,6 +19,7 @@ import java.util.ArrayList;
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;
@@ -98,7 +99,14 @@ public abstract class AbstractToolCallSupport {
}
protected List<Message> handleToolCalls(Prompt prompt, ChatResponse response) {
AssistantMessage assistantMessage = response.getResult().getOutput();
Optional<Generation> toolCallGeneration = response.getResults()
.stream()
.filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls()))
.findFirst();
if (toolCallGeneration.isEmpty()) {
throw new IllegalStateException("No tool call generation found in the response!");
}
AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();
ToolResponseMessage toolMessageResponse = this.executeFunctions(assistantMessage);
return this.buildToolCallConversation(prompt.getInstructions(), assistantMessage, toolMessageResponse);
}
@@ -180,11 +188,16 @@ public abstract class AbstractToolCallSupport {
return false;
}
var generation = generations.get(0);
return generations.stream().anyMatch(g -> isToolCall(g, toolCallFinishReasons));
}
protected boolean isToolCall(Generation generation, Set<String> toolCallFinishReasons) {
var finishReason = (generation.getMetadata().getFinishReason() != null)
? generation.getMetadata().getFinishReason() : "";
return !CollectionUtils.isEmpty(generation.getOutput().getToolCalls()) && toolCallFinishReasons.stream()
.map(s -> s.toLowerCase())
.toList()
.contains(generation.getMetadata().getFinishReason().toLowerCase());
.contains(finishReason.toLowerCase());
}
}