From d8583271c7f28dea885c11c0b96f8a1e0ba983f8 Mon Sep 17 00:00:00 2001 From: jitokim Date: Tue, 19 Nov 2024 02:13:05 +0900 Subject: [PATCH] fix callback merging in AzureOpenAiChatModel constructor and minor fixes Signed-off-by: jitokim --- .../ai/anthropic/api/AnthropicApi.java | 8 +- .../ai/anthropic/api/StreamHelper.java | 9 +- .../ai/azure/openai/AzureOpenAiChatModel.java | 7 +- .../openai/AzureOpenAiChatModelTests.java | 82 +++++++++++++++++++ .../chat/model/AbstractToolCallSupport.java | 3 +- .../functions/anthropic-chat-functions.adoc | 2 +- 6 files changed, 96 insertions(+), 15 deletions(-) create mode 100644 models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatModelTests.java diff --git a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java index 248511850..894d24945 100644 --- a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java +++ b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/AnthropicApi.java @@ -80,7 +80,7 @@ public class AnthropicApi { private final StreamHelper streamHelper = new StreamHelper(); - private WebClient webClient; + private final WebClient webClient; /** * Create a new client api with DEFAULT_BASE_URL @@ -261,7 +261,7 @@ public class AnthropicApi { /** * The CLAUDE_INSTANT_1_2 */ - CLAUDE_INSTANT_1_2("claude-instant-1.2"); + @Deprecated CLAUDE_INSTANT_1_2("claude-instant-1.2"); // @formatter:on private final String value; @@ -366,7 +366,7 @@ public class AnthropicApi { /** * Artificially created event to aggregate tool use events. */ - TOOL_USE_AGGREATE + TOOL_USE_AGGREGATE } @@ -889,7 +889,7 @@ public class AnthropicApi { @Override public EventType type() { - return EventType.TOOL_USE_AGGREATE; + return EventType.TOOL_USE_AGGREGATE; } /** diff --git a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/StreamHelper.java b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/StreamHelper.java index ec08d2030..ae62eb074 100644 --- a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/StreamHelper.java +++ b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/api/StreamHelper.java @@ -48,6 +48,7 @@ import org.springframework.util.StringUtils; * * @author Mariusz Bernacki * @author Christian Tzolov + * @author Jihoon Kim * @since 1.0.0 */ public class StreamHelper { @@ -85,10 +86,10 @@ public class StreamHelper { } } else if (event.type() == EventType.CONTENT_BLOCK_DELTA) { - ContentBlockDeltaEvent contentBolckDelta = (ContentBlockDeltaEvent) event; - if (ContentBlock.Type.INPUT_JSON_DELTA.getValue().equals(contentBolckDelta.delta().type())) { + ContentBlockDeltaEvent contentBlockDelta = (ContentBlockDeltaEvent) event; + if (ContentBlock.Type.INPUT_JSON_DELTA.getValue().equals(contentBlockDelta.delta().type())) { return eventAggregator - .appendPartialJson(((ContentBlockDeltaJson) contentBolckDelta.delta()).partialJson()); + .appendPartialJson(((ContentBlockDeltaJson) contentBlockDelta.delta()).partialJson()); } } else if (event.type() == EventType.CONTENT_BLOCK_STOP) { @@ -119,7 +120,7 @@ public class StreamHelper { .withUsage(messageStartEvent.message().usage()) .withContent(new ArrayList<>()); } - else if (event.type().equals(EventType.TOOL_USE_AGGREATE)) { + else if (event.type().equals(EventType.TOOL_USE_AGGREGATE)) { ToolUseAggregationEvent eventToolUseBuilder = (ToolUseAggregationEvent) event; if (!CollectionUtils.isEmpty(eventToolUseBuilder.getToolContentBlocks())) { diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java index 203981d90..9add97589 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java @@ -104,6 +104,7 @@ import org.springframework.util.CollectionUtils; * @author luocongqiu * @author timostark * @author Soby Chacko + * @author Jihoon Kim * @see ChatModel * @see com.azure.ai.openai.OpenAIClient * @since 1.0.0 @@ -160,7 +161,7 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha public AzureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder, AzureOpenAiChatOptions options, FunctionCallbackContext functionCallbackContext, List toolFunctionCallbacks) { - this(openAIClientBuilder, options, functionCallbackContext, List.of(), ObservationRegistry.NOOP); + this(openAIClientBuilder, options, functionCallbackContext, toolFunctionCallbacks, ObservationRegistry.NOOP); } public AzureOpenAiChatModel(OpenAIClientBuilder openAIClientBuilder, AzureOpenAiChatOptions options, @@ -235,10 +236,6 @@ public class AzureOpenAiChatModel extends AbstractToolCallSupport implements Cha Flux chatCompletionsStream = this.openAIAsyncClient .getChatCompletionsStream(options.getModel(), options); - // 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<>(); - ChatModelObservationContext observationContext = ChatModelObservationContext.builder() .prompt(prompt) .provider(AiProvider.AZURE_OPENAI.value()) diff --git a/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatModelTests.java b/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatModelTests.java new file mode 100644 index 000000000..ab3f07bb9 --- /dev/null +++ b/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureOpenAiChatModelTests.java @@ -0,0 +1,82 @@ +package org.springframework.ai.azure.openai; + +import com.azure.ai.openai.OpenAIClientBuilder; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.ai.model.function.FunctionCallback; +import org.springframework.ai.model.function.FunctionCallbackContext; + +/** + * @author Jihoon Kim + */ +@ExtendWith(MockitoExtension.class) +public class AzureOpenAiChatModelTests { + + @Mock + OpenAIClientBuilder mockClient; + + @Mock + FunctionCallbackContext functionCallbackContext; + + @Test + public void createAzureOpenAiChatModelTest() { + String callbackFromChatOptions = "callbackFromChatOptions"; + String callbackFromConstructorParam = "callbackFromConstructorParam"; + + AzureOpenAiChatOptions chatOptions = AzureOpenAiChatOptions.builder() + .withFunctionCallbacks(List.of(new TestFunctionCallback(callbackFromChatOptions))) + .build(); + + List functionCallbacks = List.of(new TestFunctionCallback(callbackFromConstructorParam)); + + AzureOpenAiChatModel openAiChatModel = new AzureOpenAiChatModel(mockClient, chatOptions, + functionCallbackContext, functionCallbacks); + + assert 2 == openAiChatModel.getFunctionCallbackRegister().size(); + + assert callbackFromChatOptions == openAiChatModel.getFunctionCallbackRegister() + .get(callbackFromChatOptions) + .getName(); + + assert callbackFromConstructorParam == openAiChatModel.getFunctionCallbackRegister() + .get(callbackFromConstructorParam) + .getName(); + } + + private class TestFunctionCallback implements FunctionCallback { + + private final String name; + + public TestFunctionCallback(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public String getInputTypeSchema() { + return null; + } + + @Override + public String call(String functionInput) { + return null; + } + + } + +} \ No newline at end of file diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java index 38ba886f6..b78a8ba7b 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java @@ -41,6 +41,7 @@ import org.springframework.util.CollectionUtils; * @author Christian Tzolov * @author Grogdunn * @author Thomas Vitale + * @author Jihoon Kim * @since 1.0.0 */ public abstract class AbstractToolCallSupport { @@ -85,7 +86,7 @@ public abstract class AbstractToolCallSupport { if (!CollectionUtils.isEmpty(functionOptions.getFunctionCallbacks())) { toolFunctionCallbacksCopy.addAll(functionOptions.getFunctionCallbacks()); - // Make sure that that function callbacks are are registered directly to the + // Make sure that that function callbacks are registered directly to the // functionCallbackRegister and not passed in the default options. functionOptions.setFunctionCallbacks(List.of()); } diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/anthropic-chat-functions.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/anthropic-chat-functions.adoc index ef83072d8..adfae3a9e 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/anthropic-chat-functions.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/functions/anthropic-chat-functions.adoc @@ -112,7 +112,7 @@ public record Request(String location, Unit unit) {} It is a best practice to annotate the request object with information such that the generated JSON schema of that function is as descriptive as possible to help the AI model pick the correct function to invoke. -The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithFunctionBeanIT.java.java[FunctionCallWithFunctionBeanIT.java] demonstrates this approach. +The link:https://github.com/spring-projects/spring-ai/blob/main/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/anthropic/tool/FunctionCallWithFunctionBeanIT.java[FunctionCallWithFunctionBeanIT.java] demonstrates this approach. ==== FunctionCallback