From dce517c92bfdb93165b3b23dee0d73b2cf264125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erhan=20Emre=20D=C4=B0=C4=9EA?= Date: Mon, 18 Mar 2024 14:51:10 +0300 Subject: [PATCH] AskStreamAsync with Function Call --- .../Application.cs | 44 ++++++++++----- src/ChatGptNet/ChatGptClient.cs | 55 +++++++++++++------ .../Extensions/ChatGptChoiceExtensions.cs | 4 +- .../Extensions/ChatGptResponseExtensions.cs | 9 ++- src/ChatGptNet/IChatGptClient.cs | 18 +++--- 5 files changed, 87 insertions(+), 43 deletions(-) diff --git a/samples/ChatGptFunctionCallingConsole/Application.cs b/samples/ChatGptFunctionCallingConsole/Application.cs index d1b751b..951cc5d 100644 --- a/samples/ChatGptFunctionCallingConsole/Application.cs +++ b/samples/ChatGptFunctionCallingConsole/Application.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Text; +using System.Text.Json; using ChatGptNet; using ChatGptNet.Extensions; using ChatGptNet.Models; @@ -91,21 +92,38 @@ public async Task ExecuteAsync() { Console.WriteLine("I'm thinking..."); - var response = await chatGptClient.AskAsync(conversationId, message, toolParameters); + //var response = await chatGptClient.AskAsync(conversationId, message, toolParameters); + ChatGptResponse chatResponse = null; + StringBuilder argument = new StringBuilder(); + var r = chatGptClient.AskStreamAsync(conversationId, message, null, toolParameters); + await foreach (var response in r) + { + /*Keep response*/ + chatResponse = response; + if (response.ContainsFunctionCalls()) + { + Console.Write(response.GetArgument()); + argument.Append(response.GetArgument()); + } + else + { + Console.Write(response.GetContent()); + } + } - if (response.ContainsFunctionCalls()) + if (chatResponse!.ContainsFunctionCalls()) { - Console.WriteLine("I have identified a function to call:"); - var functionCall = response.GetFunctionCall()!; + Console.WriteLine("I have identified a function to call:"); + var functionCall = chatResponse!.GetFunctionCall()!; Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine(functionCall.Name); - Console.WriteLine(functionCall.Arguments); + Console.WriteLine(argument.ToString()); Console.ResetColor(); // Simulates the call to the function. - var functionResponse = await GetWeatherAsync(functionCall.GetArgumentsAsJson()); + var functionResponse = await GetWeatherAsync(JsonDocument.Parse(argument.ToString())); // After the function has been called, it is necessary to add the response to the conversation. @@ -125,13 +143,11 @@ public async Task ExecuteAsync() Console.ResetColor(); // Finally, it sends the original message back to the model, to obtain a response that takes into account the function call. - response = await chatGptClient.AskAsync(conversationId, message, toolParameters); - - Console.WriteLine(response.GetContent()); - } - else - { - Console.WriteLine(response.GetContent()); + var rsp = chatGptClient.AskStreamAsync(conversationId, message, null, toolParameters); + await foreach (var response in r) + { + Console.Write(response.GetContent()); + } } Console.WriteLine(); diff --git a/src/ChatGptNet/ChatGptClient.cs b/src/ChatGptNet/ChatGptClient.cs index 64fad43..966e024 100644 --- a/src/ChatGptNet/ChatGptClient.cs +++ b/src/ChatGptNet/ChatGptClient.cs @@ -87,7 +87,7 @@ public async Task AskAsync(Guid conversationId, string message, return response; } - public async IAsyncEnumerable AskStreamAsync(Guid conversationId, string message, ChatGptParameters? parameters = null, string? model = null, bool addToConversationHistory = true, [EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable AskStreamAsync(Guid conversationId, string message, ChatGptParameters? parameters = null, ChatGptToolParameters? toolParameters = null, string? model = null, bool addToConversationHistory = true, [EnumeratorCancellation] CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); @@ -95,7 +95,7 @@ public async IAsyncEnumerable AskStreamAsync(Guid conversationI conversationId = (conversationId == Guid.Empty) ? Guid.NewGuid() : conversationId; var messages = await CreateMessageListAsync(conversationId, message, cancellationToken); - var request = CreateChatGptRequest(messages, null, true, parameters, model); + var request = CreateChatGptRequest(messages, toolParameters, true, parameters, model); var requestUri = options.ServiceConfiguration.GetChatCompletionEndpoint(model ?? options.DefaultModel); using var requestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri) @@ -114,6 +114,7 @@ public async IAsyncEnumerable AskStreamAsync(Guid conversationI using var reader = new StreamReader(responseStream); IEnumerable? promptFilterResults = null; + ChatGptFunctionCall functionCall = new ChatGptFunctionCall(); while (!reader.EndOfStream) { @@ -133,28 +134,46 @@ public async IAsyncEnumerable AskStreamAsync(Guid conversationI if (choice?.Delta is not null) { choice.Delta.Role = ChatGptRoles.Assistant; - var content = choice.Delta.Content; - - if (choice.FinishReason == ChatGptFinishReasons.ContentFilter) + if (choice.Delta.FunctionCall != null) { - // The response has been filtered by the content filtering system. Returns the response as is. - yield return response; + /* First response is always function name so we need to store this data */ + if (!String.IsNullOrWhiteSpace(choice.Delta.FunctionCall.Name)) + functionCall.Name = choice.Delta.FunctionCall.Name; + functionCall.Arguments = choice.Delta.FunctionCall.Arguments; + + /* We set delta's function call full name and streaming argument */ + choice.Delta.FunctionCall = functionCall; + + if (choice.Delta != null) + { + yield return response; + } } - else if (!string.IsNullOrEmpty(content)) + else { - // It is a normal assistant response. - if (contentBuilder.Length == 0) + /* Normal message streaming */ + var content = choice.Delta.Content; + if (choice.FinishReason == ChatGptFinishReasons.ContentFilter) { - // If this is the first response, trims all the initial special characters. - content = content.TrimStart('\n'); - choice.Delta.Content = content; + // The response has been filtered by the content filtering system. Returns the response as is. + yield return response; } - - // Yields the response only if there is an actual content. - if (content != string.Empty) + else if (!string.IsNullOrEmpty(content)) { - contentBuilder.Append(content); - yield return response; + // It is a normal assistant response. + if (contentBuilder.Length == 0) + { + // If this is the first response, trims all the initial special characters. + content = content.TrimStart('\n'); + choice.Delta.Content = content; + } + + // Yields the response only if there is an actual content. + if (content != string.Empty) + { + contentBuilder.Append(content); + yield return response; + } } } } diff --git a/src/ChatGptNet/Extensions/ChatGptChoiceExtensions.cs b/src/ChatGptNet/Extensions/ChatGptChoiceExtensions.cs index 882568e..d0f9605 100644 --- a/src/ChatGptNet/Extensions/ChatGptChoiceExtensions.cs +++ b/src/ChatGptNet/Extensions/ChatGptChoiceExtensions.cs @@ -11,14 +11,14 @@ public static class ChatGptChoiceExtensions /// Gets a value indicating whether this choice contains a function call. /// public static bool ContainsFunctionCalls(this ChatGptChoice choice) - => choice.Message?.FunctionCall is not null || (choice.Message?.ToolCalls?.Any(call => call.Type == ChatGptToolTypes.Function) ?? false); + => choice.Message?.FunctionCall is not null || choice.Delta?.FunctionCall is not null || (choice.Message?.ToolCalls?.Any(call => call.Type == ChatGptToolTypes.Function) ?? false); /// /// Gets the first function call of the message, if any. /// /// The first function call of the message, if any. public static ChatGptFunctionCall? GetFunctionCall(this ChatGptChoice choice) - => choice.Message?.FunctionCall ?? choice.Message?.ToolCalls?.FirstOrDefault(call => call.Type == ChatGptToolTypes.Function)?.Function; + => choice.Message?.FunctionCall ?? choice.Delta?.FunctionCall ?? choice.Message?.ToolCalls?.FirstOrDefault(call => call.Type == ChatGptToolTypes.Function)?.Function; /// /// Gets a value indicating whether this choice contains at least one tool call. diff --git a/src/ChatGptNet/Extensions/ChatGptResponseExtensions.cs b/src/ChatGptNet/Extensions/ChatGptResponseExtensions.cs index 23b3d87..a03137e 100644 --- a/src/ChatGptNet/Extensions/ChatGptResponseExtensions.cs +++ b/src/ChatGptNet/Extensions/ChatGptResponseExtensions.cs @@ -17,7 +17,14 @@ public static class ChatGptResponseExtensions /// public static string? GetContent(this ChatGptResponse response) => response.Choices.FirstOrDefault()?.Delta?.Content ?? response.Choices.FirstOrDefault()?.Message?.Content?.Trim(); - + /// + /// Gets the content of the called function arguments, if available. + /// + /// The content of the first choice, if available. + /// When using streaming responses, this method returns a partial message delta. + /// + public static string? GetArgument(this ChatGptResponse response) + => response.Choices.FirstOrDefault()?.Delta?.FunctionCall?.Arguments; /// /// Gets a value indicating whether the first choice, if available, contains a tool call. /// diff --git a/src/ChatGptNet/IChatGptClient.cs b/src/ChatGptNet/IChatGptClient.cs index 87c8e26..d072adb 100644 --- a/src/ChatGptNet/IChatGptClient.cs +++ b/src/ChatGptNet/IChatGptClient.cs @@ -120,6 +120,7 @@ Task AskAsync(Guid conversationId, string message, ChatGptParam /// /// The message. /// A object used to override the default completion parameters in the property. + /// A object that contains the list of available functions for calling. /// The chat completion model to use. If is , then the one specified in the property will be used. /// Set to to add the current chat interaction to the conversation history. /// The token to monitor for cancellation requests. @@ -133,8 +134,8 @@ Task AskAsync(Guid conversationId, string message, ChatGptParam /// /// /// - IAsyncEnumerable AskStreamAsync(string message, ChatGptParameters? parameters = null, string? model = null, bool addToConversationHistory = true, CancellationToken cancellationToken = default) => - AskStreamAsync(Guid.NewGuid(), message, parameters, model, addToConversationHistory, cancellationToken); + IAsyncEnumerable AskStreamAsync(string message, ChatGptParameters? parameters = null, ChatGptToolParameters? toolParameters = null, string? model = null, bool addToConversationHistory = true, CancellationToken cancellationToken = default) => + AskStreamAsync(Guid.NewGuid(), message, parameters, toolParameters, model, addToConversationHistory, cancellationToken); /// /// Requests a chat interaction with streaming response, like in ChatGPT. @@ -142,6 +143,7 @@ IAsyncEnumerable AskStreamAsync(string message, ChatGptParamete /// The unique identifier of the conversation, used to automatically retrieve previous messages in the chat history. /// The message. /// A object used to override the default completion parameters in the property. + /// A object that contains the list of available functions for calling. /// The chat completion model to use. If is , then the one specified in the property will be used. /// Set to to add the current chat interaction to the conversation history. /// The token to monitor for cancellation requests. @@ -152,7 +154,7 @@ IAsyncEnumerable AskStreamAsync(string message, ChatGptParamete /// /// /// - IAsyncEnumerable AskStreamAsync(Guid conversationId, string message, ChatGptParameters? parameters = null, string? model = null, bool addToConversationHistory = true, CancellationToken cancellationToken = default); + IAsyncEnumerable AskStreamAsync(Guid conversationId, string message, ChatGptParameters? parameters = null, ChatGptToolParameters? toolParameters = null, string? model = null, bool addToConversationHistory = true, CancellationToken cancellationToken = default); /// /// Explicitly adds a new interaction (a question and the corresponding answer) to an existing conversation history. @@ -188,7 +190,7 @@ IAsyncEnumerable AskStreamAsync(string message, ChatGptParamete /// /// /// - /// + /// Task LoadConversationAsync(IEnumerable messages, CancellationToken cancellationToken = default) => LoadConversationAsync(Guid.NewGuid(), messages, true, cancellationToken); @@ -235,7 +237,7 @@ Task LoadConversationAsync(IEnumerable messages, Cancellat /// or are . /// The conversation history is empty. /// - /// + /// /// Task AddToolResponseAsync(Guid conversationId, ChatGptFunctionCall function, string content, CancellationToken cancellationToken = default) => AddToolResponseAsync(conversationId, null, function.Name, content, cancellationToken); @@ -251,7 +253,7 @@ Task AddToolResponseAsync(Guid conversationId, ChatGptFunctionCall function, str /// or are . /// The conversation history is empty. /// - /// + /// /// Task AddToolResponseAsync(Guid conversationId, string functionName, string content, CancellationToken cancellationToken = default) => AddToolResponseAsync(conversationId, null, functionName, content, cancellationToken); @@ -267,7 +269,7 @@ Task AddToolResponseAsync(Guid conversationId, string functionName, string conte /// or are . /// The conversation history is empty. /// - /// + /// /// Task AddToolResponseAsync(Guid conversationId, ChatGptToolCall tool, string content, CancellationToken cancellationToken = default) => AddToolResponseAsync(conversationId, tool.Id, tool.Function!.Name, content, cancellationToken); @@ -284,7 +286,7 @@ Task AddToolResponseAsync(Guid conversationId, ChatGptToolCall tool, string cont /// or are . /// The conversation history is empty. /// - /// + /// /// Task AddToolResponseAsync(Guid conversationId, string? toolId, string name, string content, CancellationToken cancellationToken = default);