.NET MAUIでGPTからのレスポンスをストリーミングで受け取る方法
やりたいこと
GPTからのレスポンスを「ストリーミング形式」で受け取って取得できるのか検証してみます。
言語によっては難しい場合もあるようですので、.NET MAUIでは出来るのか気になったためです。
.NET MAUIとは
Microsoftが提供するクロスプラットフォーム開発ができるフレームワークです。
Mac / Windows / iOS / Androidといった別々のOS向けに単一のソースコーでアプリを作ることができます。
React NativeやFlutterと同じ系統ですね。
UI側はXAML、ロジック側はC#を使います。
環境構築
1. .NET SDKをインストール
.NET 8 SDKをインストールします。
以下のサイトからダウンロードできます。
ターミナルでバージョンが表示されれば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へのリクエストはアプリ側(クライアント側)から直接叩くことはお勧めしません。
MainPage.xaml.csファイル内にロジックを書いていきます。
自分が作ったAPIはマルチモーダル対応のため、リクエストボディがMultipart形式になっています。
GPTへの質問は「英語の6ヶ月で話せるようにするための効果的なロードマップをを考えてほしい。」としています。(それなりに長いレスポンスが欲しいため)
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を追加します。
<?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を使う必要があります。
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