Blazor WebAssembly から ASP.NET Core Web API を介した Azure OpenAI Service 応答ストリーム(Server-Side Events)の受信

2024/03/10
★★★

Blazor WebAssembly から中間層を介した Server-Sent Events の受信

前回、Azure OpenAI Service からのストリーム応答を ASP.NET Core Web API を介して、フロントエンド アプリで受信する方法を説明しました。

前回は、フロントエンド アプリを .NET コンソール アプリで実装しましたが、今回は、フロントエンド アプリに Blazor WebAssembly を使用した例を説明します。

Blazor WebAssembly を使用することで、C# で、SPA(Single Page Application) を実装できます。ASP.NET Core Web API 等で C# でバックエンドを開発している場合では、フロントエンドとバックエンドで同じプログラミング言語を使用できるようになります。

Blazor WebAssembly から ASP.NET Core Web API へ要求を行い、ASP.NET Core Web API では、Azure OpenAI Service から Server-Sent Events によるイベントを順次、受信し、受信するごとに、Blazor WebAssembly へ Server-Sent Events でイベントを送信する構成の実装を説明します。

Blazor WebAssembly から中間層を介した Azure OpenAI Service へアクセス

ASP.NET Core Web API による中間層の実装

中間層の実装は、前回と変更ありません。以下の ASP.NET Core Web API による中間層の実装 を確認ください。

Blazor WebAssembly のイベント受信の実装

以下のコードは、Blazor WebAssembly から、中間層への要求を行い、Server-Side Events でイベントを受信する PostStreamingAsync メソッドです。

public async Task PostStreamingAsync(
    ChatMessage[] messages, 
    Action<ChatMessage, string?> onReadStream, 
    int readingMillisecondsDelay, 
    CancellationToken cancellationToken)
{
    if (_httpClient.BaseAddress == null)
    {
        throw new InvalidOperationException("The BaseAddress of HttpClient is null.");
    }

	var json = JsonSerializer.Serialize(messages);

    HttpRequestMessage httpRequestMessage = new()
    {
        Content = new StringContent(json, Encoding.UTF8, "application/json"),
        Method = HttpMethod.Post,
        RequestUri = new Uri(_httpClient.BaseAddress, "openaimodel/streaming"),
    };

    // streaming is enabled
    httpRequestMessage.SetBrowserResponseStreamingEnabled(true);

    HttpResponseMessage response;

    try
    {
        response = await _httpClient.SendAsync(
            httpRequestMessage, 
            HttpCompletionOption.ResponseHeadersRead, 
            cancellationToken)
            .ConfigureAwait(false);
    }
    catch (Exception ex) 
    {
        _logger.LogError(ex, "An error occurred while sending the HTTP request.");
        throw;
    }

    if (!response.IsSuccessStatusCode)
    {
        throw new HttpRequestException(
            $"Response status code does not indicate success: {(int)response.StatusCode} ({response.ReasonPhrase}).");
    }

    ChatMessage chatMessage = new()
    {
        Content = string.Empty,
    };

    await foreach (var jsonNode in
        ReadSseStreamingAsync(response, cancellationToken)
        .ConfigureAwait(false))
    {
        if (jsonNode == null)
        {
            continue;
        }

        chatMessage.Id = jsonNode["id"]?.GetValue<string>();
        chatMessage.Role = jsonNode["role"]?.GetValue<string>();            
        chatMessage.CreatedDateTime = jsonNode["createdDateTime"]?.GetValue<DateTimeOffset>();

        var chunkedContent = jsonNode["content"]?.GetValue<string>();
        chatMessage.Content += chunkedContent;

        if (chatMessage is not { Id: null, Role: null, CreatedDateTime: null })
        {
            onReadStream.Invoke(chatMessage, chunkedContent);
        }

        await Task.Delay(readingMillisecondsDelay).ConfigureAwait(false);
    }        
}

処理の流れは、.NET コンソールアプリの実装と変わりありません。ただし、Blazor WebAssembly の場合の特有の設定が必要な個所があります。HttpClient.SendAsync で、要求を行っていますが、ここで、Blazor WebAssembly の場合の特有の設定が必要となります。 HttpRequestMessage の 拡張メソッド SetBrowserResponseStreamingEnabled を使用し、ストリームでの受信を有効化する必要があります。

Blazor WebAssembly の HttpClient は、各ブラウザーで実装されている Fetch API を使用しており、Fetch API の各オプションは、HttpRequestMessage の拡張メソッド WebAssemblyHttpRequestMessageExtensions を使用して設定する必要があります。

また、.NET コンソールアプリのコード例でも説明しましたが、HttpClient.SendAsync の 引数で、HttpCompletionOption.ResponseHeadersRead を設定し、ヘッダーを読み込んだ時点で、応答を返すようにする必要もあります。

要求後は、イベント受信時に、コールバック onReadStream を呼び出し、更新された ChatMessage をメソッド利用側に渡し、逐次レンダリングを行います。

以下は、ReadSseStreamingAsync のコードです。.NET コンソールアプリのコード例と同様です。Server-Send Events イベント ストリーム フォーマットの解析部分は、最小限の実装にしています。

private async IAsyncEnumerable<JsonNode?> ReadSseStreamingAsync(
    HttpResponseMessage httpResponseMessage,
    [EnumeratorCancellation] CancellationToken cancellationToken)
{
    Stream responseStream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);

    using (var streamReader = new StreamReader(responseStream))
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var line = await streamReader.ReadLineAsync().ConfigureAwait(false);

            if (string.IsNullOrEmpty(line))
            {
                continue;
            }
            else if (line == "data: [DONE]")
            {
                break;
            }
            else if (line.StartsWith("data: "))
            {
                var body = line.Substring(6, line.Length - 6);
                yield return JsonSerializer.Deserialize<JsonNode>(body);
            }
        }
    };
}

サンプルコード

今回説明したサンプル コードは、以下に掲載しています。

コンソールアプリのサンプルコードは以下に掲載しています。

以上、参考までに。

コメント (0)

コメントの投稿