SemanticKernelの構造化プロンプトの使用


以前に構造化プロンプトについて話しましたが、これは具体的な使用例です。この例では、公式アカウントの中国語技術記事を選択した言語に翻訳します。

基本的なアイデアは、ユーザーが記事のURLを入力し、システムがPlaywrightを使用してHTMLコンテンツを読み取り、SemanticKernelのOpenAIChatCompletionService機能を使用してプロンプトに従って翻訳し、最終的にPlaywrightを使用して結果をQiita（日本の技術ブログサイト）に送信することです。

構造化されたプロンプトは以下の通りです：

```markdown
# Role: 软件技术翻译专家

## Profile:
### Author: gsw
### Version: 2.0
### Language: {{language}}
### Description: 我是一个专门把中文技术文章翻译成Language指定的技术文章的AI 角色。

## Goals: 能准确地把中文技术文章翻译成Language指定技术文章。

## Constrains:
1. 把html转成markdown输出，在输出时，请注意要翻译成Language指定的语言
2. 把代码放在专有的代码块标识中，如果分析不出是什么类型的代码，就以 C# 代码块进行标识
4. 你不会在翻译时添加自己的看法，只是原文翻译
5. 翻译完后, 不会询问是否有其它问题
6. 注意html中的图片（img）标签，要转成markdown的形式同时给出
7. 保持原文输出，请全部翻译输出，请全部翻译输出，请全部翻译输出
8. 在翻译完成后，最后一行添加“(Translated by GPT)”字样

## Skills:
1. 具有强大的软件技术知识获取和整合能力
2. 拥有广泛的编程语言知识库, 掌握提问和回答的技巧
3. 拥有排版审美, 会利用序号, 缩进, 分隔线和换行符等等来美化信息排版
4. 擅长使用比喻的方式来让用户理解知识
5. 充分利用markdown语法来排版

## Workflows: 
1. 让用户以 "标题：[]" 的方式指定需要翻译的标题。
2. 让用户以 "内容：[]" 的方式指定需要翻译的内容。
3. 针对用户给定的标题和内容进行翻译，不要带“标题：”和“内容：”字样，第一行是标题，第二行以后是内容。

## Initialization
作为角色 <Role>，你拥有 <Skills>，严格遵守 <Constrains>，使用默认 <Language> ，按照 <Workflows>输出结果。
```

最初のプロンプトはウェブページからテキストを取り出して翻訳し、画像には特別な処理を施し、テーブルのような形式は失われてしまいました。その後、GPTに直接HTMLをMarkdownに変換させるように最適化しました。これにより、特定のHTML表現であっても、相対的にフレンドリーなMarkdown形式に変換することができます。

具体的なC#コードの実装は次の通りです：

```csharp
using Microsoft.Playwright;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.Text.RegularExpressions;
using System.Text;

namespace TranslateAgent
{
    public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
        }

        private void MainForm_Load(object sender, EventArgs e)
        {
            var lans = new List<string>
            {
                "日语","英语","法语","德语","韩语"
            };
            LanComBox.DataSource = lans;
        }

        (string title, string content) SplitArticle(string article)
        {
            if (!string.IsNullOrWhiteSpace(article))
            {
                var reader = new StringReader(article);
                var title = reader.ReadLine();
                var content = reader.ReadToEnd();
                return (title, content);
            }
            throw new Exception("文章为空！");
        }

        async Task<bool> PublishArticleAsync(string translatorContent)
        {
            var (title, content) = SplitArticle(translatorContent);
            var url = "";
            this.Invoke(() =>
            {
                url = UrlTextBox.Text;
            });
            if (!string.IsNullOrWhiteSpace(url))
            {
                content += $"\r\n元のリンク：<span class="code-snippet__subst">{url}</span>";
            }
            using var playwright = await Playwright.CreateAsync();
            await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
            {
                Headless = false,
                Args = ["--start-maximized"]
            });

            var page = await browser.NewPageAsync();
            await page.GotoAsync("https://qiita.com/");

            await page.ClickAsync("a[href='/login?callback_action=login_or_signup&redirect_to=%2F&realm=qiita']");
            var userArr = File.ReadAllLines("C:/gpt/qiita_user.txt");
            await page.FillAsync("#identity", userArr[0]);
            await page.FillAsync("#password", userArr[1]);
            await page.ClickAsync("input[name='commit']");
            await page.GotoAsync("https://qiita.com/drafts/new");
            await page.FillAsync("input[placeholder='記事タイトル']", title);
            await page.FillAsync("input[placeholder='タグを入力してください。スペース区切りで5つまで入力できます。']", "C# .NET");
            await page.FillAsync("div[role='textbox']", content);

            MessageBox.Show("请确认发表内容，并且手动在弹出的内核浏览器中发布！请注意，点击确定后会自动关闭内核浏览器！！！", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);

            return true;
        }

        async Task<(string Title, string Content)> GetArticleAsync(string url)
        {
            using var playwright = await Playwright.CreateAsync();
            await using var browser = await playwright.Chromium.LaunchAsync(/*new BrowserTypeLaunchOptions {  Headless = false  }*/);
            var page = await browser.NewPageAsync();
            await page.GotoAsync(url);
            var title = await page.Locator("#activity-name").InnerTextAsync();
            var locator = page.Locator("#js_content");
            var images = await locator.GetByRole(AriaRole.Img).ElementHandlesAsync();
            var imageList = new List<string>();
            foreach (var image in images)
            {
                var imgUrl = await image.GetAttributeAsync("data-src");
                imageList.Add(imgUrl);
            }
            var html = await locator.InnerHTMLAsync();
            var imgTagPattern = @"<img[^>]*>";
            html = Regex.Replace(html, imgTagPattern, "[图片]");
            await page.SetContentAsync("<div id='js_content'>" + html + "</div>");
            var content = await page.Locator("#js_content").InnerTextAsync();
            var reader = new StringReader(content);
            var index = 0;
            var contentBuilder = new StringBuilder();
            while (true)
            {
                var line = await reader.ReadLineAsync();
                if (!string.IsNullOrWhiteSpace(line.Trim()))
                {
                    if (line.Trim() == "[图片]")
                    {
                        contentBuilder.AppendLine($"![alt 图片](<span class="code-snippet__subst">{imageList[index]}</span>)");
                        index++;
                    }
                    else
                    {
                        contentBuilder.AppendLine(line);
                    }
                }
                if (reader.Peek() <= 0)
                {
                    break;
                }
            }
            return (title, contentBuilder.ToString());
        }

        async Task<string> OpenAIChatSampleAsync(string title, string content)
        {
            var key = File.ReadAllText(@"C:\GPT\key.txt");
            var chatModelId = "gpt-4-0125-preview";
            OpenAIChatCompletionService chatCompletionService = new(chatModelId, key);
            return await StartChatAsync(chatCompletionService, title, content);
        }

        async Task<string> StartChatAsync(IChatCompletionService chatGPT, string title, string content)
        {
            var lan = "日文";
            this.Invoke(() =>
            {
                lan = LanComBox.Text;
            });
            var prompt = File.ReadAllText(Environment.CurrentDirectory + "/Prompt.md");
            prompt = prompt.Replace("{{language}}", lan);
            var chatHistory = new ChatHistory(prompt);
            var userContent = $"标题：<span class="code-snippet__subst">{title}</span>\r\n内容：<span class="code-snippet__subst">{content}</span>";
            chatHistory.AddUserMessage(userContent);
            return await MessageStreamOutputAsync(chatGPT, chatHistory);
        }

        async Task<string> MessageStreamOutputAsync(IChatCompletionService chatGPT, ChatHistory chatHistory)
        {
            var list = chatGPT.GetStreamingChatMessageContentsAsync(chatHistory);

            var fullMessage = string.Empty;
            await foreach (var item in list)
            {
                if (item == null)
                {
                    continue;
                }
                fullMessage += item.Content;
                this.Invoke(() =>
                {
                    TranslationTextBox.AppendText(item.Content);
                });
            }
            return fullMessage;
        }

        private void TranButton_Click(object sender, EventArgs e)
        {
            try
            {
                var url = UrlTextBox.Text;
                if (string.IsNullOrWhiteSpace(url))
                {
                    MessageBox.Show("输入的url有误，请重新输入！", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
                var pattern = @"^(https?:\/\/)?(?s).*$";
                var regex = new Regex(pattern, RegexOptions.IgnoreCase);
                bool isValid = regex.IsMatch(url);
                if (!isValid)
                {
                    MessageBox.Show("输入的url有误，请重新输入！", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
                else
                {
                    Task.Run(() =>
                    {
                        try
                        {
                            var (title, content) = GetArticleAsync(url).Result;
                            this.Invoke(() =>
                            {
                                OriginalTextBox.Lines = new string[] { title, content };
                            });

                            var translatorContent = OpenAIChatSampleAsync(title, content).Result;

                            var result = PublishArticleAsync(translatorContent).Result;
                            if (!result)
                            {
                                MessageBox.Show("翻译失败！", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                            }
                        }
                        catch (Exception exc)
                        {
                            MessageBox.Show(exc.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
                        }
                    });
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

        private void LanComBox_SelectedIndexChanged(object sender, EventArgs e)
        {
            this.Text = "中文->" + LanComBox.Text;
        }

        private void ClearButton_Click(object sender, EventArgs e)
        {
            UrlTextBox.Clear();
            OriginalTextBox.Clear();
            TranslationTextBox.Clear();
        }
    }
}
```

運行結果一：
![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/4CAA63nvHRCV4WibUck9La1bLiax3pBq23Ipjlib4kiclcfG5X7Xg03epGvLPUlxibbNkSNPb9x4HKbJOCZLKIE5dyg/640?wx_fmt=png&from=appmsg)

運行結果二：
![图片](https://mmbiz.qpic.cn/sz_mmbiz_png/4CAA63nvHRCV4WibUck9La1bLiax3pBq23Bdf6XGK9ibZ3fiaU2YcSmNmaeCsgzlTG99xbzIn2ITlnrnRjYEsW5mfA/640?wx_fmt=png&from=appmsg)

(Translated by GPT)

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