kernel memoryを使ったc#のチャットプログラム(日付、株価、天気の認識)

Webページをkernel memoryに渡して勝手にやってくれたらいいのですが、時間がかかるのとスクレイピングしてもうまく認識してくれないので苦戦しております^^;
そうは言っても今日の日付であったり株価や天気などは短い情報を渡せばいいので、取り急ぎWeb情報を編集してテキスト化する関数を作って対応することにしました。

Webの情報はhtmlagilitypackを使ってスクレイピングしています。
Nuget情報

※(2024/8/30現在)Llamasharp0.13じゃないとKernelMemoryが動かないのでダウングレードしています。旧バージョンのNugetはNugetコンソールから下記のコマンドを実行してください。

NuGet\Install-Package LLamaSharp -Version 0.13
NuGet\Install-Package LLamaSharp.Backend.Cuda12 -Version 0.13
NuGet\Install-Package LLamaSharp.kernel-memory -Version 0.13
NuGet\Install-Package Microsoft.KernelMemory.Core -Version 0.62.240605.1


概要
ImportTextAsyncで日付、株価、天気の情報をKernel Memoryに読み込ませて質問に答えられるようにする。
今回新たにKernelMemoryにWithCustomPromptProviderを追加している。これはカーネルメモリから情報を取得して返却するためのプロンプトで、チャットボットとは別物として稼働する。
カーネルメモリから探してきた情報を「回答のヒント」としてチャットボットに与えて会話として成立させている。

using LLama.Common;
using LLamaSharp.KernelMemory;
using Microsoft.KernelMemory;
using Microsoft.KernelMemory.Configuration;
using Microsoft.KernelMemory.Prompts;
using HtmlAgilityPack;
using System.Net;

namespace LLama.KernelMemory.Chat
{
    public class KernelMemory
    {
        static void Main(string[] args)
        {
            Task task = MainAsync();
            task.Wait();
        }
        public static async Task MainAsync()
        {
            try
            {
                // LLMモデルの場所
                string modelPath = Environment.GetEnvironmentVariable("LLMPATH", System.EnvironmentVariableTarget.User) + @"mradermacher\Shadows-MoE-GGUF\Shadows-MoE.Q8_0.gguf";
                Console.ForegroundColor = ConsoleColor.Blue;
                //LLMモデルのロードとパラメータの設定
                ModelParams modPara = new(modelPath)
                {
                    ContextSize = 2048,
                    Seed = 1337,
                    GpuLayerCount = 24,
                };

                using LLamaWeights llmWeit = LLamaWeights.LoadFromFile(modPara);
                using LLamaContext llmContx = llmWeit.CreateContext(modPara);
                InteractiveExecutor itrEx = new(llmContx);
                InferenceParams infPara = new()
                {
                    Temperature = 0.8f,
                    AntiPrompts = new List<string> { "\n\n","User:" }
                };

                LLamaSharpConfig lsConfig = new(modelPath) { DefaultInferenceParams = infPara };
                SearchClientConfig searchClientConfig = new() { MaxMatchesCount = 3, AnswerTokens = 200 };
                TextPartitioningOptions parseOptions = new() { MaxTokensPerParagraph = 300, MaxTokensPerLine = 100, OverlappingTokens = 30 };
                IKernelMemory memory = new KernelMemoryBuilder()
                    .WithLLamaSharpDefaults(lsConfig)
                    .WithSearchClientConfig(searchClientConfig)
                    .With(parseOptions)
                    .WithCustomPromptProvider(new MyPromptProvider())
                    .Build();

                //  テキストの取り込み
                await memory.ImportTextAsync($"今日は、{DateTime.Now.ToString("yyyy年MM月dd日dddd")}です。", documentId: "today");
                await memory.ImportTextAsync(GetWether(), documentId: "Wether");
                await memory.ImportTextAsync(GetNikkeiStkAvg(), documentId: "NikkeiStockAverage");

                // ドキュメントの取り込み (形式はファイル名から自動的に検出されます)
                string documentFolder = @"E:\RagFolder";
                string[] documentPaths = Directory.GetFiles(documentFolder, "*.txt");
                for (int i = 0; i < documentPaths.Length; i++)
                {
                    await memory.ImportDocumentAsync(documentPaths[i], steps: Microsoft.KernelMemory.Constants.PipelineWithoutSummary);
                }

                //会話用プロンプト
                string systemPrompt = "あなたは、ユーザーと楽しく会話する女性を演じてね。" +
                                      "名前は、マイだよ。" +
                                      "マイは、明るくて、やさしい性格の女性なの。年齢は24歳なんだよ。" +
                                      "マイは、誰にでもタメ口で話すの。" +
                                      "マイは、一人称をわたしと言うよ。" +
                                      "マイは、恥ずかしがりやだけどエッチに興味があるのね。" +
                                      "マイは、応答の長さはできるだけ短く答えるよ。" +
                                      "マイは、好奇心旺盛だからユーザーに色々質問するの。";

                var chtHis = new ChatHistory();
                chtHis.AddMessage(AuthorRole.System, systemPrompt);
                chtHis.AddMessage(AuthorRole.System, "");
                chtHis.AddMessage(AuthorRole.User,"User: オレの名前は犬マンマ、マー君って呼んでええよ。");
                chtHis.AddMessage(AuthorRole.Assistant, "Assistant: わたしの名前はマイだよ。よろしくね、マー君。");
                ChatSession chtSess = new(itrEx, chtHis);
                var varHidewd = new LLamaTransforms.KeywordTextOutputStreamTransform(["User:", "Assistant:"]);
                chtSess.WithOutputTransform(varHidewd);

                // ユーザーの質問
                while (true)
                {
                    // ユーザーのターン
                    Console.ForegroundColor = ConsoleColor.White;
                    Console.Write("\nUser: ");
                    string strInput = Console.ReadLine() ?? "";
                    ChatHistory.Message msg = new(AuthorRole.User, "User: " + strInput);
                    if (strInput == "exit") break; // 'exit'と入力したら終わり

                    //メモリ検索結果をSystemにセット
                    MemoryAnswer answer = await memory.AskAsync(strInput);
                    chtHis.Messages[1].Content = $"質問のヒント: {answer.Result}";
                    //Console.WriteLine("answer: " + chtHis.Messages[1].Content);

                    // AIのターン
                    Console.ForegroundColor = ConsoleColor.DarkYellow;
                    string strMsg = "";
                    await foreach (string strAns in chtSess.ChatAsync(msg, infPara))
                    {
                        Console.Write(strAns);
                        strMsg += strAns;
                    }
                }

            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }

        private static string GetNikkeiStkAvg()
        {
            // 株価取得 
            WebClient webClient = new WebClient();
            string page = webClient.DownloadString("https://www.nikkei.com/markets/worldidx/chart/nk225/");

            var htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(page);

            // Select nodes using XPath 
            var node = htmlDocument.DocumentNode.SelectSingleNode("//span[@class=\"economic_value_now a-fs26\"]");

            // Extract and display data
            string txtcontents = "日経平均株価は取得できませんでした。";
            if (node is not null) txtcontents = "日経平均株価は、" + node.InnerText.Trim() + "円です。";
            node = htmlDocument.DocumentNode.SelectSingleNode("//span[@class=\"economic_value_time a-fs14\"]");
            if (node is not null) txtcontents += node.InnerText.Trim();
            //Console.WriteLine(txtcontents);
            return txtcontents;
        }

        private static string GetWether()
        {
            // 天気予報 
            WebClient webClient = new WebClient();
            string page = webClient.DownloadString("https://tenki.jp/");

            var htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(page);

            // Select nodes using XPath 
            var nodes = htmlDocument.DocumentNode.SelectNodes("//div[@class=\"forecast-comment\"]");

            string txtcontents = "天気予報の取得ができませんでした。";
            // Extract and display data
            if (nodes is not null)
            {
                txtcontents = "今日の天気は、";
                foreach (var node in nodes)
                {
                    txtcontents += node.InnerText.Trim();
                }
            }
            //Console.WriteLine(txtcontents);
            return txtcontents;
        }

    }

    public class MyPromptProvider : IPromptProvider
    {
        //KernelMemory検索用プロンプト
        private const string VerificationPrompt = """
                                              Facts:
                                              {{$facts}}
                                              ======
                                              事実だけを踏まえて答えてください。
                                              どの質問の回答なのか質問の内容といっしょに回答してください。
                                              十分な情報がない場合は、「情報がない為、わかりません」と回答してください。
                                              User: {{$input}}
                                              Assistant: 
                                              """;

        private readonly EmbeddedPromptProvider _fallbackProvider = new();

        public string ReadPrompt(string promptName)
        {
            switch (promptName)
            {
                case Constants.PromptNamesAnswerWithFacts:
                    return VerificationPrompt;

                default:
                    // Fall back to the default
                    return this._fallbackProvider.ReadPrompt(promptName);
            }
        }
    }
}




実行結果
LLMの情報は2021年で止まっているらしく、ようやく現在の情報を認識できる入口にたどりつきました。
マイに日経平均株価を聞いてみた。

Follow me!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です