-
Notifications
You must be signed in to change notification settings - Fork 751
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
Initial sketch of an SSE JSON serializer helper. #5557
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Text; | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization.Metadata; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.Shared.Diagnostics; | ||
using Microsoft.Shared.Pools; | ||
|
||
#pragma warning disable SA1114 // Parameter list should follow declaration | ||
|
||
namespace Microsoft.Extensions.AI; | ||
|
||
public static partial class AIJsonUtilities | ||
{ | ||
private static readonly byte[] _sseEventFieldPrefix = "event: "u8.ToArray(); | ||
private static readonly byte[] _sseDataFieldPrefix = "data: "u8.ToArray(); | ||
private static readonly byte[] _sseIdFieldPrefix = "id: "u8.ToArray(); | ||
private static readonly byte[] _sseLineBreak = Encoding.UTF8.GetBytes(Environment.NewLine); | ||
|
||
/// <summary> | ||
/// Serializes the specified server-sent events to the provided stream as JSON data. | ||
/// </summary> | ||
/// <typeparam name="T">Specifies the type of data payload in the event.</typeparam> | ||
/// <param name="stream">The UTF-8 stream to write the server-sent events to.</param> | ||
/// <param name="sseEvents">The events to serialize to the stream.</param> | ||
/// <param name="options">The options configuring serialization.</param> | ||
/// <param name="cancellationToken">The token taht can be used to cancel the write operation.</param> | ||
/// <returns>A task representing the asynchronous write operation.</returns> | ||
public static async ValueTask SerializeAsSseAsync<T>( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want this as a public API, I'd rather it be in System.Net.ServerSentEvents. For now it can be a non-public implementation detail from anything that needs to use it. I don't think we should be exposing this publicly from M.E.AI. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Do you have any particular use case in mind? Like a specific streamer for
Should I file a proposal in runtime following this shape? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The use case I've been talking about all along, being able to write out the M.E.AI object model as an OpenAI-compatible response, both non-streaming and streaming varieties (this case being the latter).
Sure. But in runtime ideally I'd want it to be something ASP.NET would rely on, so it'd be good to understand what its needs would be. |
||
Stream stream, | ||
IAsyncEnumerable<SseEvent<T>> sseEvents, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we consider an overload accepting |
||
JsonSerializerOptions? options = null, | ||
CancellationToken cancellationToken = default) | ||
{ | ||
_ = Throw.IfNull(stream); | ||
_ = Throw.IfNull(sseEvents); | ||
|
||
options ??= DefaultOptions; | ||
options.MakeReadOnly(); | ||
var typeInfo = (JsonTypeInfo<T>)options.GetTypeInfo(typeof(T)); | ||
|
||
BufferWriter<byte> bufferWriter = BufferWriterPool.SharedBufferWriterPool.Get(); | ||
|
||
try | ||
{ | ||
// Build a custom Utf8JsonWriter that ignores indentation configuration from JsonSerializerOptions. | ||
using Utf8JsonWriter writer = new(bufferWriter); | ||
|
||
await foreach (var sseEvent in sseEvents.WithCancellation(cancellationToken).ConfigureAwait(false)) | ||
{ | ||
JsonSerializer.Serialize(writer, sseEvent.Data, typeInfo); | ||
#pragma warning disable CA1849 // Call async methods when in an async method | ||
writer.Flush(); | ||
#pragma warning restore CA1849 // Call async methods when in an async method | ||
Debug.Assert(bufferWriter.WrittenSpan.IndexOf((byte)'\n') == -1, "The buffer writer should not contain any newline characters."); | ||
|
||
if (sseEvent.EventType is { } eventType) | ||
{ | ||
await stream.WriteAsync(_sseEventFieldPrefix, cancellationToken).ConfigureAwait(false); | ||
await stream.WriteAsync(Encoding.UTF8.GetBytes(eventType), cancellationToken).ConfigureAwait(false); | ||
await stream.WriteAsync(_sseLineBreak, cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
await stream.WriteAsync(_sseDataFieldPrefix, cancellationToken).ConfigureAwait(false); | ||
await stream.WriteAsync( | ||
#if NET | ||
bufferWriter.WrittenMemory, | ||
#else | ||
bufferWriter.WrittenMemory.ToArray(), | ||
#endif | ||
cancellationToken).ConfigureAwait(false); | ||
|
||
await stream.WriteAsync(_sseLineBreak, cancellationToken).ConfigureAwait(false); | ||
|
||
if (sseEvent.Id is { } id) | ||
{ | ||
await stream.WriteAsync(_sseIdFieldPrefix, cancellationToken).ConfigureAwait(false); | ||
await stream.WriteAsync(Encoding.UTF8.GetBytes(id), cancellationToken).ConfigureAwait(false); | ||
await stream.WriteAsync(_sseLineBreak, cancellationToken).ConfigureAwait(false); | ||
} | ||
|
||
await stream.WriteAsync(_sseLineBreak, cancellationToken).ConfigureAwait(false); | ||
|
||
bufferWriter.Reset(); | ||
writer.Reset(); | ||
} | ||
} | ||
finally | ||
{ | ||
BufferWriterPool.SharedBufferWriterPool.Return(bufferWriter); | ||
} | ||
} | ||
|
||
#if !NET | ||
private static Task WriteAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default) | ||
=> stream.WriteAsync(buffer, 0, buffer.Length, cancellationToken); | ||
#endif | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
#pragma warning disable SA1114 // Parameter list should follow declaration | ||
#pragma warning disable CA1815 // Override equals and operator equals on value types | ||
|
||
namespace Microsoft.Extensions.AI; | ||
|
||
/// <summary>Represents a server-sent event.</summary> | ||
/// <typeparam name="T">Specifies the type of data payload in the event.</typeparam> | ||
public readonly struct SseEvent<T> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This largely replicates the |
||
{ | ||
/// <summary>Initializes a new instance of the <see cref="SseEvent{T}"/> struct.</summary> | ||
/// <param name="data">The event's payload.</param> | ||
public SseEvent(T data) | ||
{ | ||
Data = data; | ||
} | ||
|
||
/// <summary>Gets the event's payload.</summary> | ||
public T Data { get; } | ||
|
||
/// <summary>Gets the event's type.</summary> | ||
public string? EventType { get; init; } | ||
|
||
/// <summary>Gets the event's identifier.</summary> | ||
public string? Id { get; init; } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the OS-specific line break felt appropriate given that all variants are supported by the spec.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most implementations I've seen always use '\n' regardless of OS.