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

Content filtering & GetContent() #112

Merged
merged 4 commits into from
Sep 18, 2023
Merged
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
2 changes: 1 addition & 1 deletion samples/ChatGptApi/ChatGptApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.10" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.11" />
<PackageReference Include="MinimalHelpers.OpenApi" Version="1.0.4" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion samples/ChatGptApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ await problemDetailsService.WriteAsync(new()

app.MapGet("/api/chat/stream", (Guid? conversationId, string message, IChatGptClient chatGptClient) =>
{
async IAsyncEnumerable<string> Stream()
async IAsyncEnumerable<string?> Stream()
{
// Requests a streaming response.
var responseStream = chatGptClient.AskStreamAsync(conversationId.GetValueOrDefault(), message);
Expand Down
2 changes: 1 addition & 1 deletion samples/ChatGptApi/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"ApiKey": "", // Required
//"Organization": "", // Optional, used only by OpenAI
"ResourceName": "", // Required when using Azure OpenAI Service
"ApiVersion": "2023-07-01-preview", // Optional, used only by Azure OpenAI Service (default: 2023-07-01-preview)
"ApiVersion": "2023-08-01-preview", // Optional, used only by Azure OpenAI Service (default: 2023-08-01-preview)
"AuthenticationType": "ApiKey", // Optional, used only by Azure OpenAI Service. Allowed values : ApiKey (default) or ActiveDirectory

"DefaultModel": "my-model",
Expand Down
6 changes: 3 additions & 3 deletions samples/ChatGptBlazor.Wasm/ChatGptBlazor.Wasm.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Markdig" Version="0.32.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.10" PrivateAssets="all" />
<PackageReference Include="Markdig" Version="0.33.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.11" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.11" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion samples/ChatGptConsole/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public async Task ExecuteAsync()

var response = await chatGptClient.AskAsync(conversationId, message);

Console.WriteLine(response.GetMessage());
Console.WriteLine(response.GetContent());
Console.WriteLine();
}
}
Expand Down
2 changes: 1 addition & 1 deletion samples/ChatGptConsole/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"ApiKey": "", // Required
//"Organization": "", // Optional, used only by OpenAI
"ResourceName": "", // Required when using Azure OpenAI Service
"ApiVersion": "2023-07-01-preview", // Optional, used only by Azure OpenAI Service (default: 2023-07-01-preview)
"ApiVersion": "2023-08-01-preview", // Optional, used only by Azure OpenAI Service (default: 2023-08-01-preview)
"AuthenticationType": "ApiKey", // Optional, used only by Azure OpenAI Service. Allowed values: ApiKey (default) or ActiveDirectory

"DefaultModel": "my-model",
Expand Down
2 changes: 1 addition & 1 deletion samples/ChatGptFunctionCallingConsole/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public async Task ExecuteAsync()
}
else
{
Console.WriteLine(response.GetMessage());
Console.WriteLine(response.GetContent());
}

Console.WriteLine();
Expand Down
2 changes: 1 addition & 1 deletion samples/ChatGptFunctionCallingConsole/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"ApiKey": "", // Required
//"Organization": "", // Optional, used only by OpenAI
"ResourceName": "", // Required when using Azure OpenAI Service
"ApiVersion": "2023-07-01-preview", // Optional, used only by Azure OpenAI Service (default: 2023-07-01-preview)
"ApiVersion": "2023-08-01-preview", // Optional, used only by Azure OpenAI Service (default: 2023-08-01-preview)
"AuthenticationType": "ApiKey", // Optional, used only by Azure OpenAI Service. Allowed values: ApiKey (default) or ActiveDirectory

"DefaultModel": "gpt-3.5-turbo",
Expand Down
2 changes: 1 addition & 1 deletion samples/ChatGptStreamConsole/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public async Task ExecuteAsync()

await foreach (var response in responseStream)
{
Console.Write(response.GetMessage());
Console.Write(response.GetContent());
await Task.Delay(80);
}

Expand Down
2 changes: 1 addition & 1 deletion samples/ChatGptStreamConsole/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"ApiKey": "", // Required
//"Organization": "", // Optional, used only by OpenAI
"ResourceName": "", // Required when using Azure OpenAI Service
"ApiVersion": "2023-07-01-preview", // Optional, used only by Azure OpenAI Service (default: 2023-07-01-preview)
"ApiVersion": "2023-08-01-preview", // Optional, used only by Azure OpenAI Service (default: 2023-08-01-preview)
"AuthenticationType": "ApiKey", // Optional, used only by Azure OpenAI Service. Allowed values : ApiKey (default) or ActiveDirectory

"DefaultModel": "my-model",
Expand Down
55 changes: 40 additions & 15 deletions src/ChatGptNet/ChatGptClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,13 @@ public async IAsyncEnumerable<ChatGptResponse> AskStreamAsync(Guid conversationI
{
var contentBuilder = new StringBuilder();

ChatGptUsage? usage = null;
IEnumerable<ChatGptPromptAnnotations>? promptAnnotations = null;

using (var responseStream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken))
{
using var reader = new StreamReader(responseStream);

IEnumerable<ChatGptPromptFilterResults>? promptFilterResults = null;
ChatGptChoice? previousChoice = null;

while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync() ?? string.Empty;
Expand All @@ -122,32 +122,57 @@ public async IAsyncEnumerable<ChatGptResponse> AskStreamAsync(Guid conversationI
var json = line["data: ".Length..];
var response = JsonSerializer.Deserialize<ChatGptResponse>(json, jsonSerializerOptions);

// Saves partial response fields that need to be added in the next response.
usage ??= response!.Usage;
promptAnnotations ??= response!.PromptAnnotations;
response!.ConversationId = conversationId;

var content = response!.Choices?.FirstOrDefault()?.Delta?.Content;
promptFilterResults ??= response.PromptFilterResults;
response.PromptFilterResults = promptFilterResults;

if (!string.IsNullOrEmpty(content))
var currentChoice = response.Choices?.FirstOrDefault();
var content = currentChoice?.Delta?.Content;

if (currentChoice?.FinishReason is null && currentChoice?.Delta?.Role == ChatGptRoles.Assistant && content is null)
{
// If all these conditions are met, it means that the response is still in progress. Saves the temporary choice.
previousChoice = currentChoice;
}
else if (currentChoice?.FinishReason == "content_filter" && previousChoice is not null)
{
// This is the completion of a content filter response that refers to the previous temporary choice.
// Completes the previous choice using the current one.
previousChoice.FinishReason = currentChoice.FinishReason;
previousChoice.ContentFilterResults = currentChoice.ContentFilterResults;

// Completes and yields the response.
response.Choices = new[] { previousChoice };

yield return response;

// Resets the previous choice.
previousChoice = null;
}
else if (!string.IsNullOrEmpty(content))
{
// It is a normal assistant response.
// The currentChoice variable contains the full response.
currentChoice!.Delta!.Role = ChatGptRoles.Assistant;

if (contentBuilder.Length == 0)
{
// If this is the first response, trims all the initial special characters.
content = content.TrimStart('\n');
response.Choices!.First().Delta!.Content = content;
currentChoice.Delta.Content = content;
}

// Yields the response only if there is an actual content.
if (content != string.Empty)
{
contentBuilder.Append(content);

response.ConversationId = conversationId;
response.Usage = usage;
response.PromptAnnotations = promptAnnotations;

yield return response;
}

// Resets the previous choice.
previousChoice = null;
}
}
else if (line.StartsWith("data: [DONE]"))
Expand Down Expand Up @@ -315,9 +340,9 @@ private ChatGptRequest CreateRequest(IEnumerable<ChatGptMessage> messages, ChatG
User = options.User,
};

private async Task AddAssistantResponseAsync(Guid conversationId, IList<ChatGptMessage> messages, ChatGptMessage message, CancellationToken cancellationToken = default)
private async Task AddAssistantResponseAsync(Guid conversationId, IList<ChatGptMessage> messages, ChatGptMessage? message, CancellationToken cancellationToken = default)
{
if (!string.IsNullOrWhiteSpace(message.Content?.Trim()))
if (!string.IsNullOrWhiteSpace(message?.Content?.Trim()) || message?.FunctionCall is not null)
{
// Adds the message to the cache only if it has a content.
messages.Add(message);
Expand Down
4 changes: 2 additions & 2 deletions src/ChatGptNet/Extensions/ChatGptResponseExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ public static class ChatGptResponseExtensions
/// <returns>An <see cref="IAsyncEnumerable{T}"/> that allows to enumerate all the partial message deltas.</returns>
/// <seealso cref="ChatGptResponse"/>
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "This method returns an IAsyncEnumerable")]
public static async IAsyncEnumerable<string> AsDeltas(this IAsyncEnumerable<ChatGptResponse> responses)
public static async IAsyncEnumerable<string?> AsDeltas(this IAsyncEnumerable<ChatGptResponse> responses)
{
await foreach (var response in responses)
{
yield return response.GetMessage()!;
yield return response.GetContent();
}
}
}
14 changes: 7 additions & 7 deletions src/ChatGptNet/Models/ChatGptChoice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ public class ChatGptChoice
public int Index { get; set; }

/// <summary>
/// Gets or sets the message associated with this <see cref="ChatGptChoice"/>.
/// Gets or sets the message associated with this <see cref="ChatGptChoice"/>, if any.
/// </summary>
/// <seealso cref="ChatGptChoice"/>
public ChatGptMessage Message { get; set; } = new();
/// <seealso cref="ChatGptChoice"/>
public ChatGptMessage? Message { get; set; }

/// <summary>
/// Gets or sets the content filter results for the this <see cref="ChatGptChoice"/>.
Expand All @@ -27,18 +27,18 @@ public class ChatGptChoice
public ChatGptContentFilterResults? ContentFilterResults { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the this <see cref="ChatGptChoice"/> has been filtered by content filtering system.
/// Gets or sets a value indicating whether the this <see cref="ChatGptChoice"/> has been filtered by the content filtering system.
/// </summary>
/// <seealso cref="ChatGptChoice"/>
[MemberNotNullWhen(true, nameof(ContentFilterResults))]
public bool IsChoiceFiltered => ContentFilterResults is not null
public bool IsFiltered => ContentFilterResults is not null
&& (ContentFilterResults.Hate.Filtered || ContentFilterResults.SelfHarm.Filtered || ContentFilterResults.Violence.Filtered
|| ContentFilterResults.Sexual.Filtered);

/// <summary>
/// When using streaming responses, gets or sets the partial message delta associated with this <see cref="ChatGptChoice"/>.
/// </summary>
/// <see cref="ChatGptRequest.Stream"/>
/// <see cref="ChatGptRequest.Stream"/>
public ChatGptMessage? Delta { get; set; }

/// <summary>
Expand All @@ -60,5 +60,5 @@ public class ChatGptChoice
/// <summary>
/// Gets a value indicating whether this choice contains a function call.
/// </summary>
public bool IsFunctionCall => Message.FunctionCall is not null;
public bool IsFunctionCall => Message?.FunctionCall is not null;
}
18 changes: 18 additions & 0 deletions src/ChatGptNet/Models/ChatGptContentFilterError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace ChatGptNet.Models;

/// <summary>
/// Contains information about the error occurred in the content filtering system.
/// </summary>
/// <seealso cref="ChatGptContentFilterResults"/>
public class ChatGptContentFilterError
{
/// <summary>
/// Gets or sets the error message.
/// </summary>
public string Message { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the error code.
/// </summary>
public string? Code { get; set; }
}
5 changes: 5 additions & 0 deletions src/ChatGptNet/Models/ChatGptContentFilterResults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,9 @@ public class ChatGptContentFilterResults
/// The sexual category describes language related to anatomical organs and genitals, romantic relationships, acts portrayed in erotic or affectionate terms, physical sexual acts, including those portrayed as an assault or a forced sexual violent act against one's will, prostitution, pornography, and abuse.
/// </remarks>
public ChatGptContentFilterResult Violence { get; set; } = new();

/// <summary>
/// Gets or sets the error occurred in the content filtering system, if any.
/// </summary>
public ChatGptContentFilterError? Error { get; set; }
}
1 change: 1 addition & 0 deletions src/ChatGptNet/Models/ChatGptMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class ChatGptMessage
/// This property is required for all messages except <em>assistant</em> messages with function calls.
/// </remarks>
/// <seealso cref="ChatGptRoles"/>
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public string? Content { get; set; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace ChatGptNet.Models;
/// <remarks>
/// See <see href="https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/content-filter">Content filtering</see> for more information.
/// </remarks>
public class ChatGptPromptAnnotations
public class ChatGptPromptFilterResults
{
/// <summary>
/// Gets or sets the index of the prompt to which the annotation refers.
Expand Down
36 changes: 27 additions & 9 deletions src/ChatGptNet/Models/ChatGptResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,16 @@ public class ChatGptResponse
public IEnumerable<ChatGptChoice> Choices { get; set; } = Enumerable.Empty<ChatGptChoice>();

/// <summary>
/// Gets or sets the list of prompt annotations determined by the content filtering system.
/// Gets or sets the list of prompt filter results determined by the content filtering system.
/// </summary>
[JsonPropertyName("prompt_annotations")]
public IEnumerable<ChatGptPromptAnnotations>? PromptAnnotations { get; set; }
[JsonPropertyName("prompt_filter_results")]
public IEnumerable<ChatGptPromptFilterResults>? PromptFilterResults { get; set; }

/// <summary>
/// Gets or sets a value indicating whether any prompt has been filtered by content filtering system.
/// Gets or sets a value indicating whether any prompt has been filtered by the content filtering system.
/// </summary>
[MemberNotNullWhen(true, nameof(PromptAnnotations))]
public bool IsPromptFiltered => PromptAnnotations?.Any(
[MemberNotNullWhen(true, nameof(PromptFilterResults))]
public bool IsPromptFiltered => PromptFilterResults?.Any(
p => p.ContentFilterResults.Hate.Filtered || p.ContentFilterResults.SelfHarm.Filtered || p.ContentFilterResults.Violence.Filtered
|| p.ContentFilterResults.Sexual.Filtered) ?? false;

Expand All @@ -78,17 +78,35 @@ public class ChatGptResponse
/// Gets the content of the first choice, if available.
/// </summary>
/// <returns>The content of the first choice, if available.</returns>
/// <remarks>When using streaming responses, the <see cref="GetMessage"/> property returns a partial message delta.</remarks>
/// <remarks>When using streaming responses, this method returns a partial message delta.</remarks>
/// <seealso cref="ChatGptRequest.Stream"/>
public string? GetMessage() => Choices.FirstOrDefault()?.Delta?.Content ?? Choices.FirstOrDefault()?.Message.Content?.Trim();
public string? GetContent() => Choices.FirstOrDefault()?.Delta?.Content ?? Choices.FirstOrDefault()?.Message?.Content?.Trim();

/// <summary>
/// Gets the content of the first choice, 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"/>
[Obsolete("This method will be removed in the next version. Use GetContent() instead.")]
public string? GetMessage() => GetContent();

/// <summary>
/// Gets a value indicating whether the first choice, if available, has been filtered by the content filtering system.
/// </summary>
/// <seealso cref="ChatGptChoice"/>
/// <seealso cref="ChatGptChoice.IsFiltered"/>
public bool IsContentFiltered => Choices.FirstOrDefault()?.IsFiltered ?? false;

/// <summary>
/// Gets a value indicating whether the first choice, if available, contains a function call.
/// </summary>
/// <seealso cref="GetFunctionCall"/>
/// <seealso cref="ChatGptFunctionCall"/>
public bool IsFunctionCall => Choices.FirstOrDefault()?.IsFunctionCall ?? false;

/// <summary>
/// Gets or sets the function call for the message of the first choice, if available.
/// </summary>
public ChatGptFunctionCall? GetFunctionCall() => Choices.FirstOrDefault()?.Message.FunctionCall;
public ChatGptFunctionCall? GetFunctionCall() => Choices.FirstOrDefault()?.Message?.FunctionCall;
}
Loading