Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AskStreamAsync with Function Call #154

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 30 additions & 14 deletions samples/ChatGptFunctionCallingConsole/Application.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json;
using System.Text;
using System.Text.Json;
using ChatGptNet;
using ChatGptNet.Extensions;
using ChatGptNet.Models;
Expand Down Expand Up @@ -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.

Expand All @@ -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();
Expand Down
55 changes: 37 additions & 18 deletions src/ChatGptNet/ChatGptClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ public async Task<ChatGptResponse> AskAsync(Guid conversationId, string message,
return response;
}

public async IAsyncEnumerable<ChatGptResponse> AskStreamAsync(Guid conversationId, string message, ChatGptParameters? parameters = null, string? model = null, bool addToConversationHistory = true, [EnumeratorCancellation] CancellationToken cancellationToken = default)
public async IAsyncEnumerable<ChatGptResponse> AskStreamAsync(Guid conversationId, string message, ChatGptParameters? parameters = null, ChatGptToolParameters? toolParameters = null, string? model = null, bool addToConversationHistory = true, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(message);

// Ensures that conversationId isn't empty.
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)
Expand All @@ -114,6 +114,7 @@ public async IAsyncEnumerable<ChatGptResponse> AskStreamAsync(Guid conversationI
using var reader = new StreamReader(responseStream);

IEnumerable<ChatGptPromptFilterResults>? promptFilterResults = null;
ChatGptFunctionCall functionCall = new ChatGptFunctionCall();

while (!reader.EndOfStream)
{
Expand All @@ -133,28 +134,46 @@ public async IAsyncEnumerable<ChatGptResponse> 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;
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/ChatGptNet/Extensions/ChatGptChoiceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ public static class ChatGptChoiceExtensions
/// Gets a value indicating whether this choice contains a function call.
/// </summary>
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);

/// <summary>
/// Gets the first function call of the message, if any.
/// </summary>
/// <returns>The first function call of the message, if any.</returns>
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;

/// <summary>
/// Gets a value indicating whether this choice contains at least one tool call.
Expand Down
9 changes: 8 additions & 1 deletion src/ChatGptNet/Extensions/ChatGptResponseExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ public static class ChatGptResponseExtensions
/// <seealso cref="ChatGptRequest.Stream"/>
public static string? GetContent(this ChatGptResponse response)
=> response.Choices.FirstOrDefault()?.Delta?.Content ?? response.Choices.FirstOrDefault()?.Message?.Content?.Trim();

/// <summary>
/// Gets the content of the called function arguments, if available.
/// </summary>
/// <returns>The content of the first choice, if available.</returns>
/// <remarks>When using streaming responses, this method returns a partial message delta.</remarks>
/// <seealso cref="ChatGptRequest.Stream"/>
public static string? GetArgument(this ChatGptResponse response)
=> response.Choices.FirstOrDefault()?.Delta?.FunctionCall?.Arguments;
/// <summary>
/// Gets a value indicating whether the first choice, if available, contains a tool call.
/// </summary>
Expand Down
18 changes: 10 additions & 8 deletions src/ChatGptNet/IChatGptClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ Task<ChatGptResponse> AskAsync(Guid conversationId, string message, ChatGptParam
/// </summary>
/// <param name="message">The message.</param>
/// <param name="parameters">A <see cref="ChatGptParameters"/> object used to override the default completion parameters in the <see cref="ChatGptOptions.DefaultParameters"/> property.</param>
/// <param name="toolParameters">A <see cref="ChatGptToolParameters"/> object that contains the list of available functions for calling.</param>
/// <param name="model">The chat completion model to use. If <paramref name="model"/> is <see langword="null"/>, then the one specified in the <see cref="ChatGptOptions.DefaultModel"/> property will be used.</param>
/// <param name="addToConversationHistory">Set to <see langword="true"/> to add the current chat interaction to the conversation history.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
Expand All @@ -133,15 +134,16 @@ Task<ChatGptResponse> AskAsync(Guid conversationId, string message, ChatGptParam
/// <seealso cref="ChatGptRequest"/>
/// <seealso cref="ChatGptResponse"/>
/// <seealso cref="ChatGptParameters"/>
IAsyncEnumerable<ChatGptResponse> AskStreamAsync(string message, ChatGptParameters? parameters = null, string? model = null, bool addToConversationHistory = true, CancellationToken cancellationToken = default) =>
AskStreamAsync(Guid.NewGuid(), message, parameters, model, addToConversationHistory, cancellationToken);
IAsyncEnumerable<ChatGptResponse> 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);

/// <summary>
/// Requests a chat interaction with streaming response, like in ChatGPT.
/// </summary>
/// <param name="conversationId">The unique identifier of the conversation, used to automatically retrieve previous messages in the chat history.</param>
/// <param name="message">The message.</param>
/// <param name="parameters">A <see cref="ChatGptParameters"/> object used to override the default completion parameters in the <see cref="ChatGptOptions.DefaultParameters"/> property.</param>
/// <param name="toolParameters">A <see cref="ChatGptToolParameters"/> object that contains the list of available functions for calling.</param>
/// <param name="model">The chat completion model to use. If <paramref name="model"/> is <see langword="null"/>, then the one specified in the <see cref="ChatGptOptions.DefaultModel"/> property will be used.</param>
/// <param name="addToConversationHistory">Set to <see langword="true"/> to add the current chat interaction to the conversation history.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
Expand All @@ -152,7 +154,7 @@ IAsyncEnumerable<ChatGptResponse> AskStreamAsync(string message, ChatGptParamete
/// <seealso cref="ChatGptRequest"/>
/// <seealso cref="ChatGptResponse"/>
/// <seealso cref="ChatGptParameters"/>
IAsyncEnumerable<ChatGptResponse> AskStreamAsync(Guid conversationId, string message, ChatGptParameters? parameters = null, string? model = null, bool addToConversationHistory = true, CancellationToken cancellationToken = default);
IAsyncEnumerable<ChatGptResponse> AskStreamAsync(Guid conversationId, string message, ChatGptParameters? parameters = null, ChatGptToolParameters? toolParameters = null, string? model = null, bool addToConversationHistory = true, CancellationToken cancellationToken = default);

/// <summary>
/// Explicitly adds a new interaction (a question and the corresponding answer) to an existing conversation history.
Expand Down Expand Up @@ -188,7 +190,7 @@ IAsyncEnumerable<ChatGptResponse> AskStreamAsync(string message, ChatGptParamete
/// </remarks>
/// <seealso cref="ChatGptOptions.MessageLimit"/>
/// <seealso cref="AskAsync(Guid, string, ChatGptParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="AskStreamAsync(Guid, string, ChatGptParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="AskStreamAsync(Guid, string, ChatGptParameters?, ChatGptToolParameters?, string?, bool, CancellationToken)"/>
Task<Guid> LoadConversationAsync(IEnumerable<ChatGptMessage> messages, CancellationToken cancellationToken = default)
=> LoadConversationAsync(Guid.NewGuid(), messages, true, cancellationToken);

Expand Down Expand Up @@ -235,7 +237,7 @@ Task<Guid> LoadConversationAsync(IEnumerable<ChatGptMessage> messages, Cancellat
/// <exception cref="ArgumentNullException"><see cref="ChatGptFunction.Name"/> or <paramref name="content"/> are <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The conversation history is empty.</exception>
/// <seealso cref="AskAsync(Guid, string, ChatGptParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="AskStreamAsync(Guid, string, ChatGptParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="AskStreamAsync(Guid, string, ChatGptParameters?,ChatGptToolParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="ChatGptFunctionCall"/>
Task AddToolResponseAsync(Guid conversationId, ChatGptFunctionCall function, string content, CancellationToken cancellationToken = default)
=> AddToolResponseAsync(conversationId, null, function.Name, content, cancellationToken);
Expand All @@ -251,7 +253,7 @@ Task AddToolResponseAsync(Guid conversationId, ChatGptFunctionCall function, str
/// <exception cref="ArgumentNullException"><paramref name="functionName"/> or <paramref name="content"/> are <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The conversation history is empty.</exception>
/// <seealso cref="AskAsync(Guid, string, ChatGptParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="AskStreamAsync(Guid, string, ChatGptParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="AskStreamAsync(Guid, string, ChatGptParameters?, ChatGptToolParameters?,string?, bool, CancellationToken)"/>
/// <seealso cref="ChatGptFunctionCall"/>
Task AddToolResponseAsync(Guid conversationId, string functionName, string content, CancellationToken cancellationToken = default)
=> AddToolResponseAsync(conversationId, null, functionName, content, cancellationToken);
Expand All @@ -267,7 +269,7 @@ Task AddToolResponseAsync(Guid conversationId, string functionName, string conte
/// <exception cref="ArgumentNullException"><see cref="ChatGptToolCall.Function"/> or <paramref name="content"/> are <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The conversation history is empty.</exception>
/// <seealso cref="AskAsync(Guid, string, ChatGptParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="AskStreamAsync(Guid, string, ChatGptParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="AskStreamAsync(Guid, string, ChatGptParameters?, ChatGptToolParameters?,string?, bool, CancellationToken)"/>
/// <seealso cref="ChatGptToolCall"/>
Task AddToolResponseAsync(Guid conversationId, ChatGptToolCall tool, string content, CancellationToken cancellationToken = default)
=> AddToolResponseAsync(conversationId, tool.Id, tool.Function!.Name, content, cancellationToken);
Expand All @@ -284,7 +286,7 @@ Task AddToolResponseAsync(Guid conversationId, ChatGptToolCall tool, string cont
/// <exception cref="ArgumentNullException"><paramref name="name"/> or <paramref name="content"/> are <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">The conversation history is empty.</exception>
/// <seealso cref="AskAsync(Guid, string, ChatGptParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="AskStreamAsync(Guid, string, ChatGptParameters?, string?, bool, CancellationToken)"/>
/// <seealso cref="AskStreamAsync(Guid, string, ChatGptParameters?, ChatGptToolParameters?,string?, bool, CancellationToken)"/>
/// <seealso cref="ChatGptToolCall"/>
Task AddToolResponseAsync(Guid conversationId, string? toolId, string name, string content, CancellationToken cancellationToken = default);

Expand Down