SemanticKernelのプラグインの使用方法

プラグインはSKで魔法のような機能です。大規模な言語モデルが非決定的であると言いますが、私たちのコードは決定的です。そして、プラグインはこの非決定性を決定性に変える機能を持っています。

以下の例は、**自然言語での購入**を実装したケースです。顧客はテキストまたは音声で自然言語を入力し、
```xml
<ItemGroup>
  <PackageReference Include="Microsoft.SemanticKernel" Version="1.7.1" />
  <PackageReference Include="NAudio" Version="2.2.1" />
</ItemGroup>
```
次に、具体的なコードです。アクセスポイントを定義する際、KernelFunction属性とDescription属性を使って、これがプラグインであること、そしてDescriptionの記述を使用して自然言語の意味をマッピングし、パラメータを抽出することで、以下のようになります。
```csharp
#pragma warning disable SKEXP0001
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.AudioToText;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.TextToAudio;
using NAudio.Wave;
using System.ComponentModel;
using System.Data;

namespace LittleHelper
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }
        StoreSystem _store;
        Kernel _kernel;
        IChatCompletionService _chatCompletionService;
        OpenAIPromptExecutionSettings _openAIPromptExecutionSettings;
        ChatHistory _history;
        string _key;
        WaveOutEvent _player;
        private void MainForm_Load(object sender, EventArgs e)
        {
            _store = new StoreSystem();
            TotalLab.Text = "总价：" + _store.Total.ToString("0.00");
            GoodsGrid.DataSource = _store.GoodsList;
            GoodsGrid.Rows[0].Cells[0].Selected = false;
            var chatModelId = "gpt-4-0125-preview";
            _key = File.ReadAllText(@"C:\GPT\key.txt");

            var builder = Kernel.CreateBuilder();
            builder.Services.AddOpenAIChatCompletion(chatModelId, _key);
            builder.Plugins.AddFromObject(_store);
            _kernel = builder.Build();
            _history = new ChatHistory("所有水果的单价(Price)单位是斤)，所有数量都是斤。");
            _chatCompletionService = _kernel.GetRequiredService<IChatCompletionService>();
            _openAIPromptExecutionSettings = new OpenAIPromptExecutionSettings()
            {
                ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
            };
            _player = new WaveOutEvent();
        }
        private void SubmitButton_Click(object sender, EventArgs e)
        {
            if (string.IsNullOrWhiteSpace(AskTextBox.Text))
            {
                MessageBox.Show("提示不能为空！");
                return;
            }
            var askText = AskTextBox.Text;
            AskTextBox.Clear();
            ResultTextBox.Clear();
            InMessageLab.Text = "输入问题：\r\n" + askText;
            AskQuestion(askText);
        }
        void ReloadGrid()
        {
            GoodsGrid.DataSource = null;
            GoodsGrid.DataSource = _store.GoodsList;
            foreach (DataGridViewColumn col in GoodsGrid.Columns)
            {
                switch (col.DataPropertyName)
                {
                    case "Name":
                        col.HeaderText = "名称";
                        col.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
                        break;
                    case "Price":
                        col.HeaderText = "单价";
                        break;
                    case "Quantity":
                        col.HeaderText = "库存数量";
                        break;
                    case "BuyQuantity":
                        col.HeaderText = "销售数量";
                        break;
                }
            }
            foreach (DataGridViewRow row in GoodsGrid.Rows)
            {
                if (row.Cells["BuyQuantity"].Value != null)
                {
                    var buyQuantity = (int)row.Cells["BuyQuantity"].Value;
                    if (buyQuantity > 0)
                    {
                        row.Cells["BuyQuantity"].Style.BackColor = Color.LightGreen;
                    }
                }
            }
            GridClearSelect();
            TotalLab.Text = "总价：" + _store.Total.ToString("0.00");
        }
        async Task AskQuestion(string askMessage)
        {
            _history.AddUserMessage(askMessage);
            var result = await _chatCompletionService.GetChatMessageContentAsync(_history, _openAIPromptExecutionSettings, _kernel);

            var fullMessage = result.Content;
            ResultTextBox.Text = fullMessage;
            _history.AddMessage(AuthorRole.Assistant, fullMessage);

            await TextToAudioAsync(fullMessage);
            ReloadGrid();
        }
        async Task TextToAudioAsync(string speakText)
        {
            var kernel = Kernel.CreateBuilder()
                .AddOpenAITextToAudio(
                    modelId: "tts-1",
                    apiKey: _key)
                .Build();
            var textToAudioService = kernel.GetRequiredService<ITextToAudioService>();
            //音声ファイルに変換
            var executionSettings = new OpenAITextToAudioExecutionSettings("shimmer")
            {
                ResponseFormat = "mp3",
                Speed = 1.0f
            };
            var audioContent = await textToAudioService.GetAudioContentAsync(speakText, executionSettings);
            var outputFolder = Path.Combine(Directory.GetCurrentDirectory(), "NAudio");
            Directory.CreateDirectory(outputFolder);
            var speakFile = DateTime.Now.ToString("yyyyMMddHHmmssfff") + ".mp3";
            var audioFilePath = Path.Combine(outputFolder, speakFile);         
            await File.WriteAllBytesAsync(audioFilePath, audioContent.Data.Value.ToArray());

            //音声ファイルを再生
            using var reader = new AudioFileReader(audioFilePath);
            _player.Init(reader);
            _player.Play();
        }
        WaveInEvent waveIn;
        bool audioMark = true;
        string outputFilePath = "audio.wav";
        WaveFileWriter writer;
        private void SpeekBut_Click(object sender, EventArgs e)
        {
            if (audioMark)
            {
                SpeekBut.Text = "停止语音";
                var recordFile = DateTime.Now.ToString("yyyyMMddHHmmssfff") + ".wav";
                var outputFolder = Path.Combine("C://GPT/NAudio");
                Directory.CreateDirectory(outputFolder);
                outputFilePath = Path.Combine(outputFolder, recordFile);

                if (waveIn == null)
                {
                    waveIn = new WaveInEvent();
                    if (writer == null)
                    {
                        writer = new WaveFileWriter(outputFilePath, waveIn.WaveFormat);
                    }

                    waveIn.DataAvailable += (s, a) =>
                    {
                        if (writer != null)
                        {
                            writer.Write(a.Buffer, 0, a.BytesRecorded);
                        }
                    };
                }

                waveIn.StartRecording();
            }
            else
            {
                SpeekBut.Text = "开始语音";
                waveIn.StopRecording();

                writer?.Dispose();
                writer = null;
                waveIn?.Dispose();
                waveIn = null;

                AudioToTextAsync(outputFilePath).ContinueWith(t =>
                {
                    if (t.IsFaulted)
                    {
                        MessageBox.Show("转换失败！");
                    }
                    else
                    {
                        var text = t.Result;
                        this.Invoke(() =>
                        {
                            AskTextBox.Text = text;
                            AskQuestion(text);
                        });
                    }
                });
            }
            audioMark = !audioMark;
        }
        private async Task<string> AudioToTextAsync(string audioFilePath)
        {
            try
            {
                var kernel = Kernel.CreateBuilder()
                    .AddOpenAIAudioToText(
                        modelId: "whisper-1",
                        apiKey: _key)
                    .Build();

                var audioToTextService = kernel.GetRequiredService<IAudioToTextService>();

                var executionSettings = new OpenAIAudioToTextExecutionSettings(audioFilePath)
                {
                    Language = "zh",
                    Prompt = "给出简体中文的文本",
                    ResponseFormat = "json",
                    Temperature = 0.3f,
                    Filename = "audio.wav"
                };

                ReadOnlyMemory<byte> audioData = await File.ReadAllBytesAsync(audioFilePath);
                var audioContent = new AudioContent(new BinaryData(audioData));
                var textContent = await audioToTextService.GetTextContentAsync(audioContent, executionSettings);
                return textContent?.Text;
            }
            catch (Exception exc)
            {
                return exc.Message;
            }
        }
        private void GoodsGrid_Leave(object sender, EventArgs e)
        {
            GridClearSelect();
        }
        void GridClearSelect()
        {
            foreach (DataGridViewRow row in GoodsGrid.Rows)
            {
                row.Selected = false;
                foreach (DataGridViewCell cell in row.Cells)
                {
                    cell.Selected = false;
                }
            }
        }
    }
    public class StoreSystem
    {
        public List<Goods> GoodsList { get; set; } = new List<Goods>
        {
            new Goods("苹果",5,100),
            new Goods("香蕉",3,200),
            new Goods("橙子",4,150),
            new Goods("桃子",6,120),
            new Goods("梨",5,100),
            new Goods("葡萄",7,80),
            new Goods("西瓜",8,60),
            new Goods("菠萝",9,40),
            new Goods("芒果",10,30),
            new Goods("草莓",11,20),
            new Goods("柠檬",4,100),
            new Goods("橘子",3,100),
            new Goods("蓝莓",6,100),
            new Goods("樱桃",7,100),
            new Goods("葡萄柚",8,100),
            new Goods("柚子",9,100),
            new Goods("榴莲",10,100),
            new Goods("火龙果",11,100),
            new Goods("荔枝",12,100),
            new Goods("椰子",13,100),
            new Goods("桑葚",5,100),
            new Goods("杨梅",4,100),
            new Goods("树梅",6,100),
            new Goods("莓子",7,100),
            new Goods("石榴",8,100),
            new Goods("蜜桃",9,100),
        };
        public decimal Total { get; set; } = 0;
        [KernelFunction]
        [Description("按照水果名称(Name)查询水果")]
        public string GetGoodsByName([Description("水果名称")] string name)
        {
            return GoodsList.FirstOrDefault(g => g.Name == name)?.ToString() ?? "未找到水果";
        }
        [KernelFunction]
        [Description("查询单价(Price)少于等于参数的所有水果")]
        public string GetGoodsLessEqualsPrice([Description("水果单价")] decimal price)
        {
            var goodses = GoodsList.Where(g => g.Price <= price);
            if (goodses == null || goodses.Any() == false)
            {
                return "未找到水果";
            }
            else
            {
                return string.Join("\n", goodses);
            }
        }
        [Description("查询单价(Price)少于参数的所有水果")]
        public string GetGoodsLessPrice([Description("水果单价")] decimal price)
        {
            var goodses = GoodsList.Where(g => g.Price < price);
            if (goodses == null || goodses.Any() == false)
            {
                return "未找到水果";
            }
            else
            {
                return string.Join("\n", goodses);
            }
        }
        [KernelFunction]
        [Description("查询单价(Price)大于等于参数的所有水果")]
        public string GetGoodsGreaterEqualsPrice([Description("水果单价")] decimal price)
        {
            var goodses = GoodsList.Where(g => g.Price >= price);
            if (goodses == null || goodses.Any() == false)
            {
                return "未找到水果";
            }
            else
            {
                return string.Join("\n", goodses);
            }
        }
        [KernelFunction]
        [Description("查询单价(Price)大于参数的所有水果")]
        public string GetGoodsGreaterPrice([Description("水果单价")] decimal price)
        {
            var goodses = GoodsList.Where(g => g.Price > price);
            if (goodses == null || goodses.Any() == false)
            {
                return "未找到水果";
            }
            else
            {
                return string.Join("\n", goodses);
            }
        }
        [KernelFunction]
        [Description("查询库存数量(Quantity)大于等于参数的所有水果")]
        public string GetGoodsGreaterEqualsQuantity([Description("水果库存数量")] int quantity)
        {
            var goodses = GoodsList.Where(g => g.Quantity >= quantity);
            if (goodses == null || goodses.Any() == false)
            {
                return "未找到水果";
            }
            else
            {
                return string.Join("\n", goodses);
            }
        }
        [KernelFunction]
        [Description("查询库存数量(Quantity)大于参数的所有水果")]
        public string GetGoodsGreaterQuantity([Description("水果库存数量")] int quantity)
        {
            var goodses = GoodsList.Where(g => g.Quantity > quantity);
            if (goodses == null || goodses.Any() == false)
            {
                return "未找到水果";
            }
            else
            {
                return string.Join("\n", goodses);
            }
        }
        [KernelFunction]
        [Description("查询库存数量(Quantity)少于等于参数的所有水果")]
        public string GetGoodsLessEqualsQuantity([Description("水果数量")] int quantity)
        {
            var goodses = GoodsList.Where(g => g.Quantity <= quantity);
            if (goodses == null || goodses.Any() == false)
            {
                return "未找到水果";
            }
            else
            {
                return string.Join("\n", goodses);
            }
        }
        [KernelFunction]
        [Description("查询库存数量(Quantity)少于参数的所有水果")]
        public string GetGoodsLessQuantity([Description("水果数量")] int quantity)
        {
            var goodses = GoodsList.Where(g => g.Quantity < quantity);
            if (goodses == null || goodses.Any() == false)
            {
                return "未找到水果";
            }
            else
            {
                return string.Join("\n", goodses);
            }
        }
        [KernelFunction]
        [Description("购买水果")]
        public string BuyGoods([Description("水果名称")] string name, [Description("购买数量")] int quantity)
        {
            var goods = GoodsList.FirstOrDefault(g => g.Name == name);
            if (goods != null)
            {
                var newQuantity = goods.Quantity - quantity;
                if (newQuantity < 0)
                {
                    return "库存不足";
                }
                else
                {
                    goods.Quantity = newQuantity;
                    goods.BuyQuantity += quantity;
                    Total += goods.Price * quantity;
                    return "购买成功！";
                }
            }
            else
            {
                return "未找到水果";
            }
        }
    }
    public class Goods
    {
        public Goods(string name, decimal price, int quantity)
        {
            Name = name;
            Price = price;
            Quantity = quantity;
        }

        public string Name { get; set; }
        public decimal Price { get; set; }
        public int Quantity { get; set; }
        public int BuyQuantity { get; set; } = 0;

        public override string ToString()
        {
            return $"名称(Name):{Name}, 単価(Price):{Price}, 在庫数量(Quantity):{Quantity}, 販売数量(BuyQuantity):{BuyQuantity}";
        }
    }
}
```
![購入記録の結果](https://mmbiz.qpic.cn/sz_mmbiz_png/4CAA63nvHRAoUIZub5yC5BRcnUCA4flpiccfb8cmrKN5gficJicsO6Q81ZGAPWHmueMcjfRlUYKhgeDgeib3xpr71g/640?wx_fmt=png&from=appmsg)

(Translated by GPT)

元のリンク：https://mp.weixin.qq.com/s?__biz=MzA3NDM1MzIyMQ==&mid=2247488073&idx=1&sn=d16a11d2fb89a56afb4a3265a6261291&chksm=9f004d63a877c4758c55158ec2136622ed2575a0c333e6846ef52bb56da95f05f9fc7d9191a6&token=1106494811&lang=zh_CN#rd&wt.mc_id=MVP_325642