🐨

.NET MAUIでGPTからのレスポンスをストリーミングで受け取る方法

2024/06/29に公開

やりたいこと

GPTからのレスポンスを「ストリーミング形式」で受け取って取得できるのか検証してみます。
言語によっては難しい場合もあるようですので、.NET MAUIでは出来るのか気になったためです。

.NET MAUIとは

Microsoftが提供するクロスプラットフォーム開発ができるフレームワークです。
Mac / Windows / iOS / Androidといった別々のOS向けに単一のソースコーでアプリを作ることができます。
React NativeやFlutterと同じ系統ですね。
UI側はXAML、ロジック側はC#を使います。

https://learn.microsoft.com/ja-jp/dotnet/maui/what-is-maui?view=net-maui-8.0

環境構築

1. .NET SDKをインストール

.NET 8 SDKをインストールします。
以下のサイトからダウンロードできます。

https://dotnet.microsoft.com/ja-jp/learn/maui/first-app-tutorial/install


ターミナルでバージョンが表示されればOK


2. VS Code上の設定

VS Code上で以下の拡張機能をインストール。
VS Code上からアプリを起動したりデバッグができるようになります。


3. mauiをインストール

以下のコマンドを実行

sudo dotnet workload install maui


以下のように「正常にインストールされました」と表示されたらOK

実装

1. プロジェクトを作成

VS Code上からプロジェクトを作ります。
Macの場合は「CMD + Shift + P」、Winの場合は「Ctl + Shift + p」を叩いてメニューを出します。
一覧の中から「.NET プロジェクトの作成」を選択


次に「.NET MAUIアプリ」を選択


プロジェクト名を適当に決める


あとはプロジェクトを格納するディレクトリを選択してください。


2. 必要なパッケージを導入

GPTからのレスポンスはMarkdown形式で表示させたいので、専用のパッケージを入れます。
ターミナルで対象のプロジェクトまで移動して、以下のコマンドを実行

 dotnet add package Markdig  


以下のよう<PJ名>.csprojファイルに導入されていればOK


3. ターゲットデバイスを指定

今回はiPhone15 Pro上に起動したいと思います。
VS Codeの下側にある「{}」をクリック


次に「デバッグターゲット」をクリック


AndroidやMac OS上も選択できますが、対象のiPhone15 Proを見つけて選択
※Xcodeのバージョンが古いとiOS向けにビルドができないようです。最低でもバージョン15以上に上げておきましょう


4. ロジックを実装

以下の記事で作ったFastAPI製のAPIを叩くようにします。
GPTへのリクエストはアプリ側(クライアント側)から直接叩くことはお勧めしません。

https://zenn.dev/headwaters/articles/bf9532e71fb844


MainPage.xaml.csファイル内にロジックを書いていきます。
自分が作ったAPIはマルチモーダル対応のため、リクエストボディがMultipart形式になっています。
GPTへの質問は「英語の6ヶ月で話せるようにするための効果的なロードマップをを考えてほしい。」としています。(それなりに長いレスポンスが欲しいため)

MainPage.xaml.cs
using System.Net.Http.Json;
using System.Text;
using System.Threading.Tasks;  
using System.Diagnostics;
using Markdig;

namespace <your project>;

public partial class MainPage : ContentPage
{
    HttpClient _client;

    public MainPage()
    {
        InitializeComponent();
        _client = new HttpClient();
    }

    private async void OnSendApi(object sender, EventArgs e) {
		await PostMessageToApiAsync();  
    }

    private async Task PostMessageToApiAsync() {
        try {
            Console.WriteLine("PostMessageToApiAsync");
            var multipartContent = new MultipartFormDataContent();  
            var request = new HttpRequestMessage(HttpMethod.Post, "<your endpoint>")
            {
                Content = multipartContent
            };
            multipartContent.Add(new StringContent("英語の6ヶ月で話せるようにするための効果的なロードマップをを考えてほしい。"), "message");
            HttpResponseMessage response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
            if (response.IsSuccessStatusCode) {
                var responseStream = await response.Content.ReadAsStreamAsync();
                using var streamReader = new StreamReader(responseStream);
                string? line;
                string contentAll = "";
                while (!streamReader.EndOfStream)
                {
                    line = await streamReader.ReadLineAsync();
                    Console.WriteLine(line);
                    string htmlContent = Markdown.ToHtml(line);
                    contentAll += htmlContent;
                    Dispatcher.Dispatch(() =>  
                    {  
                        MarkdownWebView.Source = new HtmlWebViewSource{Html = $"<html><body>{contentAll}</body></html>"};
                    });
                }
            }
        } catch (Exception ex) {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}


5. UI側を実装

UI側はGPTにリクエストを送るボタンと、レスポンスを表示するWebviewを追加します。

MainPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="tutorial_maui.MainPage">

    <ScrollView>
        <VerticalStackLayout
            Padding="30,0"
            Spacing="25">
            <Image
                Source="dotnet_bot.png"
                HeightRequest="185"
                Aspect="AspectFit"
                SemanticProperties.Description="dot net bot in a race car number eight" />

            <Label
                Text="Hello, MAUI!"
                Style="{StaticResource Headline}"
                SemanticProperties.HeadingLevel="Level1" />

            <Button
                x:Name="FetchBtn"
                Text="GPTリクエスト" 
                SemanticProperties.Hint="Counts the fetch of times you click"
                Clicked="OnSendApi"
                HorizontalOptions="Fill" />

            <VerticalStackLayout>  
                <WebView x:Name="MarkdownWebView"  
                        HeightRequest="500" />  
            </VerticalStackLayout>  
        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

実装はこれで完了です。

検証

VS Code上の左側にある「実行とデバッグ」をクリックして、対象が「.NET MAUI」になっていることを確認して緑色の実行ボタンをクリック


アプリが立ち上がりました。


Markdown形式でテキストをリアルタイムに表示させることができました。


デバッグコンソールを見てもストリーミングで取得できているのがわかります。

ハマったところ

HttpClientのPostAsyncだと上手くストリーミングで取得ができませんでした。

どうやらPOSTメソッドでリクエストするタイミングで「HttpCompletionOption.ResponseHeadersRead」を設定する必要があるようです。
postAsyncだと引数に設定ができなかったのでsendAsyncを使う必要があります。

MainPage.xaml.cs
var multipartContent = new MultipartFormDataContent();
// HttpResponseMessage response = await _client.PostAsync(<your endpoint>, multipartContent);
var request = new HttpRequestMessage(HttpMethod.Post, "<your endpoint>")
{
    Content = multipartContent
};
multipartContent.Add(new StringContent("英語の6ヶ月で話せるようにするための効果的なロードマップをを考えてほしい。"), "message");
HttpResponseMessage response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
ヘッドウォータース

Discussion