Semantic Kernel のチャットでスケジュール管理ができるツールを作る

「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に切り替えると結構スムースに指示が反映されます。

カテゴリー: 開発 パーマリンク