From 600feeee1ff41aae2d00b38bec19922ce937c9a3 Mon Sep 17 00:00:00 2001 From: jgarrison929 Date: Wed, 11 Mar 2026 20:34:01 -0700 Subject: [PATCH] Fix Bedrock connector to merge consecutive tool call/result messages (#13647) When using Bedrock with models that support parallel tool calling (e.g. Claude Sonnet), SK creates separate ChatMessageContent items for each tool call and each tool result. Bedrock's Converse API requires all tool_use blocks from one assistant turn in a single message and all tool_result blocks in a single user message. Changes: - BedrockModelUtilities.BuildMessageList: Rewritten to handle FunctionCallContent (-> ToolUse blocks), FunctionResultContent (-> ToolResult blocks), and merge consecutive same-role messages. - BedrockModelUtilities.MapAuthorRoleToConversationRole: Handle AuthorRole.Tool by mapping to ConversationRole.User. - BedrockChatCompletionClient.CreateChatMessageContentItemCollection: Handle ToolUse blocks in responses by creating FunctionCallContent items, enabling the auto-invoke tool calling loop. - Added comprehensive unit tests for parallel tool call scenarios. Fixes #13647 --- .../Core/BedrockModelUtilitiesTests.cs | 380 ++++++++++++++++++ .../Bedrock/Core/BedrockModelUtilities.cs | 163 +++++++- .../Clients/BedrockChatCompletionClient.cs | 88 +++- 3 files changed, 618 insertions(+), 13 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.Amazon.UnitTests/Core/BedrockModelUtilitiesTests.cs diff --git a/dotnet/src/Connectors/Connectors.Amazon.UnitTests/Core/BedrockModelUtilitiesTests.cs b/dotnet/src/Connectors/Connectors.Amazon.UnitTests/Core/BedrockModelUtilitiesTests.cs new file mode 100644 index 000000000000..4e00a010f7d5 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.Amazon.UnitTests/Core/BedrockModelUtilitiesTests.cs @@ -0,0 +1,380 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Amazon.BedrockRuntime; +using Amazon.BedrockRuntime.Model; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace Microsoft.SemanticKernel.Connectors.Amazon.UnitTests; + +/// +/// Unit tests for BedrockModelUtilities, specifically the BuildMessageList method +/// and its handling of tool call / tool result merging. +/// +public sealed class BedrockModelUtilitiesTests +{ + /// + /// Verifies that simple text messages are converted correctly. + /// + [Fact] + public void BuildMessageListShouldConvertSimpleTextMessages() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello"); + chatHistory.AddAssistantMessage("Hi there"); + chatHistory.AddUserMessage("How are you?"); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Equal(3, messages.Count); + Assert.Equal(ConversationRole.User, messages[0].Role); + Assert.Equal("Hello", messages[0].Content[0].Text); + Assert.Equal(ConversationRole.Assistant, messages[1].Role); + Assert.Equal("Hi there", messages[1].Content[0].Text); + Assert.Equal(ConversationRole.User, messages[2].Role); + Assert.Equal("How are you?", messages[2].Content[0].Text); + } + + /// + /// Verifies that system messages are excluded from the message list. + /// + [Fact] + public void BuildMessageListShouldExcludeSystemMessages() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("You are an AI assistant"); + chatHistory.AddUserMessage("Hello"); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Single(messages); + Assert.Equal(ConversationRole.User, messages[0].Role); + Assert.Equal("Hello", messages[0].Content[0].Text); + } + + /// + /// Verifies that consecutive assistant messages with FunctionCallContent are merged into a single message. + /// This is the core scenario from issue #13647. + /// + [Fact] + public void BuildMessageListShouldMergeConsecutiveAssistantToolCalls() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Get feeds and distribution list"); + + // First assistant message with a tool call + var assistantMessage1 = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage1.Items.Add(new FunctionCallContent( + functionName: "GetFeeds", + pluginName: "Feeds", + id: "tooluse_G64hibpFmRqXEcAYwOfP5s")); + chatHistory.Add(assistantMessage1); + + // Second assistant message with another tool call (parallel tool invocation) + var assistantMessage2 = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage2.Items.Add(new FunctionCallContent( + functionName: "GetTeamsDistributionList", + pluginName: "DistributionList", + id: "tooluse_sMmlRWbbCGd0lnhiLjvk8H", + arguments: new KernelArguments { { "distributionListName", "developers" } })); + chatHistory.Add(assistantMessage2); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Equal(2, messages.Count); // user + single merged assistant + + // The assistant message should have been merged + var assistantMsg = messages[1]; + Assert.Equal(ConversationRole.Assistant, assistantMsg.Role); + Assert.Equal(2, assistantMsg.Content.Count); + + // First tool use + Assert.NotNull(assistantMsg.Content[0].ToolUse); + Assert.Equal("tooluse_G64hibpFmRqXEcAYwOfP5s", assistantMsg.Content[0].ToolUse.ToolUseId); + Assert.Equal("Feeds-GetFeeds", assistantMsg.Content[0].ToolUse.Name); + + // Second tool use + Assert.NotNull(assistantMsg.Content[1].ToolUse); + Assert.Equal("tooluse_sMmlRWbbCGd0lnhiLjvk8H", assistantMsg.Content[1].ToolUse.ToolUseId); + Assert.Equal("DistributionList-GetTeamsDistributionList", assistantMsg.Content[1].ToolUse.Name); + } + + /// + /// Verifies that consecutive tool result messages are merged into a single user message. + /// This is part of the scenario from issue #13647. + /// + [Fact] + public void BuildMessageListShouldMergeConsecutiveToolResults() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Get feeds and distribution list"); + + // Assistant with tool calls (single message with multiple items) + var assistantMessage = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage.Items.Add(new FunctionCallContent( + functionName: "GetFeeds", + pluginName: "Feeds", + id: "tooluse_G64hibpFmRqXEcAYwOfP5s")); + assistantMessage.Items.Add(new FunctionCallContent( + functionName: "GetTeamsDistributionList", + pluginName: "DistributionList", + id: "tooluse_sMmlRWbbCGd0lnhiLjvk8H", + arguments: new KernelArguments { { "distributionListName", "developers" } })); + chatHistory.Add(assistantMessage); + + // First tool result + var toolResult1 = new ChatMessageContent(AuthorRole.Tool, (string?)null); + toolResult1.Items.Add(new FunctionResultContent( + functionName: "GetFeeds", + pluginName: "Feeds", + callId: "tooluse_G64hibpFmRqXEcAYwOfP5s", + result: "Dataset DataReference: memory://8df2be02a23c4ebfb77e1170dea6c4b4. Dataset size: 78 items.")); + chatHistory.Add(toolResult1); + + // Second tool result + var toolResult2 = new ChatMessageContent(AuthorRole.Tool, (string?)null); + toolResult2.Items.Add(new FunctionResultContent( + functionName: "GetTeamsDistributionList", + pluginName: "DistributionList", + callId: "tooluse_sMmlRWbbCGd0lnhiLjvk8H", + result: "19:791df3464b4f4c3fac8429041e8e2540@thread.v2")); + chatHistory.Add(toolResult2); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Equal(3, messages.Count); // user + assistant + single merged tool results (as user) + + // Tool results should be merged into a single user message + var toolResultMsg = messages[2]; + Assert.Equal(ConversationRole.User, toolResultMsg.Role); // Tool role maps to User in Bedrock + Assert.Equal(2, toolResultMsg.Content.Count); + + // First tool result + Assert.NotNull(toolResultMsg.Content[0].ToolResult); + Assert.Equal("tooluse_G64hibpFmRqXEcAYwOfP5s", toolResultMsg.Content[0].ToolResult.ToolUseId); + Assert.Equal("Dataset DataReference: memory://8df2be02a23c4ebfb77e1170dea6c4b4. Dataset size: 78 items.", + toolResultMsg.Content[0].ToolResult.Content[0].Text); + + // Second tool result + Assert.NotNull(toolResultMsg.Content[1].ToolResult); + Assert.Equal("tooluse_sMmlRWbbCGd0lnhiLjvk8H", toolResultMsg.Content[1].ToolResult.ToolUseId); + Assert.Equal("19:791df3464b4f4c3fac8429041e8e2540@thread.v2", + toolResultMsg.Content[1].ToolResult.Content[0].Text); + } + + /// + /// Full round-trip test matching the exact scenario from issue #13647: + /// separate assistant tool-call messages + separate tool-result messages. + /// After merging, the Bedrock message list should have properly coalesced messages. + /// + [Fact] + public void BuildMessageListShouldProduceValidBedrockMessagesForParallelToolCalls() + { + // Arrange: Reproduce the exact ChatHistory from the issue + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Audit the feeds and get the developers distribution list"); + + // Two separate assistant messages with tool calls (as SK creates them) + var assistantMsg1 = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMsg1.Items.Add(new FunctionCallContent( + functionName: "GetFeeds", + pluginName: "Feeds", + id: "tooluse_G64hibpFmRqXEcAYwOfP5s")); + chatHistory.Add(assistantMsg1); + + var assistantMsg2 = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMsg2.Items.Add(new FunctionCallContent( + functionName: "GetTeamsDistributionList", + pluginName: "DistributionList", + id: "tooluse_sMmlRWbbCGd0lnhiLjvk8H", + arguments: new KernelArguments { { "distributionListName", "developers" } })); + chatHistory.Add(assistantMsg2); + + // Two separate tool result messages + var toolResult1 = new ChatMessageContent(AuthorRole.Tool, (string?)null); + toolResult1.Items.Add(new FunctionResultContent( + functionName: "GetFeeds", + pluginName: "Feeds", + callId: "tooluse_G64hibpFmRqXEcAYwOfP5s", + result: "Dataset DataReference: memory://8df2be02a23c4ebfb77e1170dea6c4b4. Dataset size: 78 items.")); + chatHistory.Add(toolResult1); + + var toolResult2 = new ChatMessageContent(AuthorRole.Tool, (string?)null); + toolResult2.Items.Add(new FunctionResultContent( + functionName: "GetTeamsDistributionList", + pluginName: "DistributionList", + callId: "tooluse_sMmlRWbbCGd0lnhiLjvk8H", + result: "19:791df3464b4f4c3fac8429041e8e2540@thread.v2")); + chatHistory.Add(toolResult2); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert: Should produce exactly 3 messages: user, assistant (merged), user/tool-results (merged) + Assert.Equal(3, messages.Count); + + // Message 1: User prompt + Assert.Equal(ConversationRole.User, messages[0].Role); + Assert.Single(messages[0].Content); + Assert.Equal("Audit the feeds and get the developers distribution list", messages[0].Content[0].Text); + + // Message 2: Merged assistant with both tool use blocks + Assert.Equal(ConversationRole.Assistant, messages[1].Role); + Assert.Equal(2, messages[1].Content.Count); + Assert.NotNull(messages[1].Content[0].ToolUse); + Assert.NotNull(messages[1].Content[1].ToolUse); + + // Verify tool use IDs are preserved correctly + var toolUseIds = messages[1].Content.Select(c => c.ToolUse.ToolUseId).ToList(); + Assert.Contains("tooluse_G64hibpFmRqXEcAYwOfP5s", toolUseIds); + Assert.Contains("tooluse_sMmlRWbbCGd0lnhiLjvk8H", toolUseIds); + + // Message 3: Merged tool results (as user role) + Assert.Equal(ConversationRole.User, messages[2].Role); + Assert.Equal(2, messages[2].Content.Count); + Assert.NotNull(messages[2].Content[0].ToolResult); + Assert.NotNull(messages[2].Content[1].ToolResult); + + // Verify tool result IDs match the tool use IDs + var toolResultIds = messages[2].Content.Select(c => c.ToolResult.ToolUseId).ToList(); + Assert.Contains("tooluse_G64hibpFmRqXEcAYwOfP5s", toolResultIds); + Assert.Contains("tooluse_sMmlRWbbCGd0lnhiLjvk8H", toolResultIds); + } + + /// + /// Verifies that Tool role maps to User in the Bedrock conversation role. + /// + [Fact] + public void MapAuthorRoleToConversationRoleShouldMapToolToUser() + { + // Act + var result = Core.BedrockModelUtilities.MapAuthorRoleToConversationRole(AuthorRole.Tool); + + // Assert + Assert.Equal(ConversationRole.User, result); + } + + /// + /// Verifies that FunctionCallContent without a plugin name uses just the function name. + /// + [Fact] + public void BuildMessageListShouldUseOnlyFunctionNameWhenNoPluginName() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Do something"); + + var assistantMessage = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage.Items.Add(new FunctionCallContent( + functionName: "MyFunction", + id: "tool_123")); + chatHistory.Add(assistantMessage); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Equal(2, messages.Count); + Assert.NotNull(messages[1].Content[0].ToolUse); + Assert.Equal("MyFunction", messages[1].Content[0].ToolUse.Name); + } + + /// + /// Verifies that tool call arguments are properly converted to Document. + /// + [Fact] + public void BuildMessageListShouldConvertFunctionCallArguments() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Search for something"); + + var assistantMessage = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage.Items.Add(new FunctionCallContent( + functionName: "Search", + pluginName: "Web", + id: "tool_456", + arguments: new KernelArguments + { + { "query", "semantic kernel" }, + { "maxResults", 10 } + })); + chatHistory.Add(assistantMessage); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + var toolUse = messages[1].Content[0].ToolUse; + Assert.NotNull(toolUse); + Assert.Equal("Web-Search", toolUse.Name); + Assert.True(toolUse.Input.IsDictionary()); + var inputDict = toolUse.Input.AsDictionary(); + Assert.Equal("semantic kernel", inputDict["query"].AsString()); + Assert.Equal(10, inputDict["maxResults"].AsInt()); + } + + /// + /// Verifies that non-consecutive same-role messages are NOT merged. + /// + [Fact] + public void BuildMessageListShouldNotMergeNonConsecutiveSameRoleMessages() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Hello"); + chatHistory.AddAssistantMessage("Hi"); + chatHistory.AddUserMessage("How are you?"); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert: All three messages should be separate + Assert.Equal(3, messages.Count); + } + + /// + /// Verifies that mixed text and tool content in a single assistant message works. + /// + [Fact] + public void BuildMessageListShouldHandleAssistantMessageWithTextAndToolCall() + { + // Arrange + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("What's the weather?"); + + var assistantMessage = new ChatMessageContent(AuthorRole.Assistant, (string?)null); + assistantMessage.Items.Add(new TextContent("Let me check the weather for you.")); + assistantMessage.Items.Add(new FunctionCallContent( + functionName: "GetWeather", + pluginName: "Weather", + id: "tool_789", + arguments: new KernelArguments { { "city", "Seattle" } })); + chatHistory.Add(assistantMessage); + + // Act + var messages = Core.BedrockModelUtilities.BuildMessageList(chatHistory); + + // Assert + Assert.Equal(2, messages.Count); + var assistantMsg = messages[1]; + Assert.Equal(2, assistantMsg.Content.Count); + Assert.Equal("Let me check the weather for you.", assistantMsg.Content[0].Text); + Assert.NotNull(assistantMsg.Content[1].ToolUse); + Assert.Equal("Weather-GetWeather", assistantMsg.Content[1].ToolUse.Name); + } +} diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockModelUtilities.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockModelUtilities.cs index dac71cd8e142..4a884dfe02c1 100644 --- a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockModelUtilities.cs +++ b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/BedrockModelUtilities.cs @@ -5,6 +5,7 @@ using System.Linq; using Amazon.BedrockRuntime; using Amazon.BedrockRuntime.Model; +using Amazon.Runtime.Documents; using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Connectors.Amazon.Core; @@ -32,6 +33,12 @@ internal static ConversationRole MapAuthorRoleToConversationRole(AuthorRole role return ConversationRole.Assistant; } + // Tool role maps to User in Bedrock (tool results are sent as user messages). + if (role == AuthorRole.Tool) + { + return ConversationRole.User; + } + throw new ArgumentOutOfRangeException($"Invalid role: {role}"); } @@ -50,27 +57,159 @@ internal static List GetSystemMessages(ChatHistory chatHisto /// /// Creates the list of user and assistant messages for the Converse Request from the Chat History. + /// Handles FunctionCallContent (tool use) and FunctionResultContent (tool result) items, + /// and merges consecutive messages with the same Bedrock role into a single message. + /// This is required because Bedrock expects all tool_use blocks from one assistant turn + /// in a single message and all tool_result blocks in a single user message. /// /// The ChatHistory object to be building the message list from. /// The list of messages for the converse request. - /// Thrown if invalid last message in chat history. + /// Thrown if the chat history is empty. internal static List BuildMessageList(ChatHistory chatHistory) { - // Check that the text from the latest message in the chat history is not empty. Verify.NotNullOrEmpty(chatHistory); - string? text = chatHistory[chatHistory.Count - 1].Content; - if (string.IsNullOrWhiteSpace(text)) + + var messages = new List(); + + foreach (var chatMessage in chatHistory) { - throw new ArgumentException("Last message in chat history was null or whitespace."); + if (chatMessage.Role == AuthorRole.System) + { + continue; + } + + var bedrockRole = MapAuthorRoleToConversationRole(chatMessage.Role); + var contentBlocks = BuildContentBlocks(chatMessage); + + if (contentBlocks.Count == 0) + { + continue; + } + + // Merge with the previous message if it has the same role. + // This handles consecutive assistant tool-call messages and consecutive tool-result messages. + if (messages.Count > 0 && messages[messages.Count - 1].Role == bedrockRole) + { + messages[messages.Count - 1].Content.AddRange(contentBlocks); + } + else + { + messages.Add(new Message + { + Role = bedrockRole, + Content = contentBlocks + }); + } } - return chatHistory - .Where(m => m.Role != AuthorRole.System) - .Select(m => new Message + + return messages; + } + + /// + /// Builds the list of Bedrock ContentBlock objects from a ChatMessageContent, + /// handling text, FunctionCallContent (ToolUse), and FunctionResultContent (ToolResult). + /// + /// The chat message to convert. + /// A list of ContentBlock objects. + private static List BuildContentBlocks(ChatMessageContent chatMessage) + { + var contentBlocks = new List(); + + foreach (var item in chatMessage.Items) + { + switch (item) { - Role = MapAuthorRoleToConversationRole(m.Role), - Content = [new() { Text = m.Content }] - }) - .ToList(); + case FunctionCallContent functionCall: + contentBlocks.Add(new ContentBlock + { + ToolUse = new ToolUseBlock + { + ToolUseId = functionCall.Id, + Name = functionCall.PluginName is not null + ? $"{functionCall.PluginName}-{functionCall.FunctionName}" + : functionCall.FunctionName, + Input = ConvertArgumentsToDocument(functionCall.Arguments) + } + }); + break; + + case FunctionResultContent functionResult: + contentBlocks.Add(new ContentBlock + { + ToolResult = new ToolResultBlock + { + ToolUseId = functionResult.CallId, + Content = [new ToolResultContentBlock { Text = functionResult.Result?.ToString() ?? string.Empty }] + } + }); + break; + + case TextContent textContent: + if (!string.IsNullOrEmpty(textContent.Text)) + { + contentBlocks.Add(new ContentBlock { Text = textContent.Text }); + } + break; + + default: + // For other content types, fall back to using ToString. + var text = item.ToString(); + if (!string.IsNullOrEmpty(text)) + { + contentBlocks.Add(new ContentBlock { Text = text }); + } + break; + } + } + + // If no items were processed but there's text content on the message itself, use that. + if (contentBlocks.Count == 0 && !string.IsNullOrEmpty(chatMessage.Content)) + { + contentBlocks.Add(new ContentBlock { Text = chatMessage.Content }); + } + + return contentBlocks; + } + + /// + /// Converts KernelArguments to an Amazon.Runtime.Documents.Document + /// for use as ToolUseBlock input. + /// + /// The arguments to convert. + /// A Document representing the arguments as a JSON-like structure. + private static Document ConvertArgumentsToDocument(KernelArguments? arguments) + { + if (arguments == null || arguments.Count == 0) + { + return new Document(new Dictionary()); + } + + var dict = new Dictionary(); + foreach (var kvp in arguments) + { + dict[kvp.Key] = ConvertValueToDocument(kvp.Value); + } + + return new Document(dict); + } + + /// + /// Converts a single value to a Document, handling common types. + /// + private static Document ConvertValueToDocument(object? value) + { + return value switch + { + null => new Document(), + string s => new Document(s), + bool b => new Document(b), + int i => new Document(i), + long l => new Document(l), + float f => new Document(f), + double d => new Document(d), + decimal dec => new Document((double)dec), + _ => new Document(value.ToString() ?? string.Empty) + }; } /// diff --git a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Clients/BedrockChatCompletionClient.cs b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Clients/BedrockChatCompletionClient.cs index fde1f2c0af3d..b8eb8d8047bd 100644 --- a/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Clients/BedrockChatCompletionClient.cs +++ b/dotnet/src/Connectors/Connectors.Amazon/Bedrock/Core/Clients/BedrockChatCompletionClient.cs @@ -144,11 +144,97 @@ private static ChatMessageContentItemCollection CreateChatMessageContentItemColl var itemCollection = new ChatMessageContentItemCollection(); foreach (var contentBlock in contentBlocks) { - itemCollection.Add(new TextContent(contentBlock.Text)); + if (contentBlock.ToolUse != null) + { + var toolUse = contentBlock.ToolUse; + KernelArguments? arguments = null; + if (toolUse.Input.IsDictionary()) + { + arguments = ParseDocumentToArguments(toolUse.Input); + } + + // Parse plugin name and function name from the tool name. + // The Bedrock connector uses "PluginName-FunctionName" format. + string? pluginName = null; + string functionName = toolUse.Name ?? string.Empty; + var separatorIndex = functionName.IndexOf('-'); + if (separatorIndex > 0) + { + pluginName = functionName.Substring(0, separatorIndex); + functionName = functionName.Substring(separatorIndex + 1); + } + + itemCollection.Add(new FunctionCallContent( + functionName: functionName, + pluginName: pluginName, + id: toolUse.ToolUseId, + arguments: arguments)); + } + else if (!string.IsNullOrEmpty(contentBlock.Text)) + { + itemCollection.Add(new TextContent(contentBlock.Text)); + } } return itemCollection; } + /// + /// Parses an Amazon.Runtime.Documents.Document to KernelArguments. + /// + private static KernelArguments? ParseDocumentToArguments(global::Amazon.Runtime.Documents.Document document) + { + if (!document.IsDictionary()) + { + return null; + } + + var arguments = new KernelArguments(); + foreach (var kvp in document.AsDictionary()) + { + arguments[kvp.Key] = DocumentToObject(kvp.Value); + } + + return arguments; + } + + /// + /// Converts a Document to a plain object for use in KernelArguments. + /// + private static object? DocumentToObject(global::Amazon.Runtime.Documents.Document doc) + { + if (doc.IsNull()) + { + return null; + } + + if (doc.IsString()) + { + return doc.AsString(); + } + + if (doc.IsBool()) + { + return doc.AsBool(); + } + + if (doc.IsInt()) + { + return doc.AsInt(); + } + + if (doc.IsLong()) + { + return doc.AsLong(); + } + + if (doc.IsDouble()) + { + return doc.AsDouble(); + } + + return doc.ToString(); + } + internal async IAsyncEnumerable StreamChatMessageAsync( ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null,