「Azure OpenAI Service 入門」を Semantic Kernel で書き換えるシリーズの第4弾です。
書籍の第8章では、いろいろな実行環境で OpenAI を使ってみようということで、デスクトップやスマホアプリ、ブラウザアプリ、シングルページアプリケーションなど作成しています。色々コードが散ってしまうのが読者…というか筆者が大変だったので「スケジュール管理ツール」1本に絞っています。
一般的なスケジュール管理では、項目を追加・削除するボタンで操作をしますが、このスケジュール管理ツールでは自然言語を使って項目を操作します。「4/1 は休日にして」とか、「4/10 の項目を消して」という具合ですね。あらかじめ、スケジュールのデータを AI 側に保持させておいて、それに対して指示を出すので、正確に変更されるとは限らない!のが最大の欠点でありますが、まあ、チャットツールを作るときの良い練習にはなると思います。
書籍のほうでは、常に成功しているように見えますが、執筆時には成功するように指示文を作るところに苦労しています。
画面はシンプル
画面はシンプルで「送信」と「保存」ボタンしかありません。指示をテキストボックスにいれて「送信」するだけです。

MVVMパターンを使う
MVVMパターンのために CommunityToolkit.Mvvm パッケージをいれておきます。
<ItemGroup>
<PackageReference Include="Microsoft.SemanticKernel" Version="1.14.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
</ItemGroup>
ViewModel クラス
コンストラクタはこんな感じ。
public class PromptViewModel : ObservableObject
{
private const string _model = "test-x";
private string _apikey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? "";
private const string _url = "https://sample-moonmile-openai.openai.azure.com/";
private Kernel _kernel;
private IChatCompletionService _service;
/// <summary>
/// コンストラクタ
/// </summary>
public PromptViewModel()
{
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
_model,
_url,
_apikey);
_kernel = builder.Build();
_service = _kernel.GetRequiredService<IChatCompletionService>();
this.SendCommand = new RelayCommand(this.Send);
this.SaveCommand = new RelayCommand(this.Save);
}
最初にあらかじめスケジュールを送信しておくのがミソです。本来ならば、ここの予定表はファイルから読み込めばいいのですが、端折っています。
/// <summary>
/// 最初のプロンプトを送信する
/// </summary>
public async void SendInit()
{
_history.AddSystemMessage(
"""
箇条書きで予定表を作ってください。
予定表のフォーマット:
- [日付] [内容]
現在の予定は以下の通りです。
[予定表はここから]
- 4/1 入社式
- 4/2 新人歓迎会
- 4/3 プログラム研修1
- 4/4 プログラム研修2
[予定表はここまで]
予定表を表示してください。
""");
var response = await _service.GetChatMessageContentAsync(
_history,
kernel: _kernel);
// 応答を取得
string combinedResponse = response.Items.OfType<TextContent>().FirstOrDefault()?.Text ?? "";
this.Output = combinedResponse;
// AIの応答を履歴に追加
_history.AddAssistantMessage(combinedResponse);
}
送信ボタンを押した時は、ヒストリも含めて送信します。
/// <summary>
/// プロンプトを送信する
/// </summary>
public async void Send()
{
_history.AddUserMessage(Input);
var response = await _service.GetChatMessageContentAsync(
_history,
kernel: _kernel);
// 応答を取得
string combinedResponse = response.Items.OfType<TextContent>().FirstOrDefault()?.Text ?? "";
this.Output = combinedResponse;
// AIの応答を履歴に追加
_history.AddAssistantMessage(this.Output);
}
ファイルに保存されるのは、最後の応答だけで構いません。最後の応答に最新のスケジュールが含まれるからです。
/// <summary>
/// 最後の回答をストレージに保存
/// </summary>
public void Save()
{
// ここでは簡便のためメッセージとして表示させる
var msg = _history.Last()?.Content;
// 保存ダイアログを開く
var dlg = new Microsoft.Win32.SaveFileDialog();
dlg.FileName = "schedule";
dlg.DefaultExt = ".txt";
dlg.Filter = "Text documents (.txt)|*.txt";
if ( dlg.ShowDialog() == true )
{
var filename = dlg.FileName;
System.IO.File.WriteAllText(filename, msg);
MessageBox.Show("保存しました。", "AIスケジューラー");
}
}
実行してみる
こんな感じに誕生日を追加することができます。執筆時は GPT-3.5で動きが悪かったのですが、GPT-4oに切り替えると結構スムースに指示が反映されます。
