AIサーバー試作品1.9

今回のは、音声+Discord Botバージョンです。音声会話の部分は1.8からの移植です。

Discord用のチャンネルを判別するパラメータが追加しています



Nuget情報
・音声のみ1.8からはDSharpPlusが追加になっています(Windows.Media.SpeechRecognitionは、Projectファイルを手修正したため一覧に出てきません。)


概要
音声会話する場合は、起動した時点では寝ていますのでAwakeキーワードに指定された”おきて”とか”おきなさい”とか話すとAIがあいさつしてきます。5分話さないと答えなくなります。Awakeキーワードで会話が再開できます。逆に「さよなら」や「おやすみ」というと寝ます。30秒間話しかけないとAIの方から質問してきます。機嫌によって話しかけない場合もあります。
App.Settingで音声ソフトの設定をしてください。対象は、CeVIOAIとVOICEVOXです。VOICEVOXの場合、プロセスにないと起動させるコマンドを追加していますが、どこにインストールされているのか探すコードを書くのがめんどくさかったので共通変数にしています。自分の環境はデバッグと本番のディレクトリ構成が違うため環境変数やらいっぱい使ってるので直書きかApp.Settingに直してください。
Discordは、Botが参加しているサーバーのチャンネルで文字チャットすると音声と同じAIが、文字チャットで返してきます。音声チャンネルが開いていると音声+文字で返してきます。こちらのモードは、外出している時や文字チャットでAIと会話したい時に使用します。

システム行(sq=1):「??」を会話トリガーに、「(_ _)」を初っ端のあいさつトリガーにしています。

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

Program.cs メイン
・音声のみと違ってDiscord用の音声ファイルを返してくるように修正しています。booPlay=trueの場合は、ローカルで音声を再生します。
2024/10/22 Op = falseのままでした。Op = trueに変更しました。
2024/10/24 AsleepをAI会話の前へ移動。Func Chatの音声をSpeechRecognitionが拾ってしまうためSleep(3000)を追加。

using System.Diagnostics;
using LLama;
using LLama.Common;
using LLama.Sampling;
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.VoiceNext;
using DSharpPlus.EventArgs;
using Windows.Media.SpeechRecognition;
using System.Text.RegularExpressions;
using System.Media;
using System.Net.Http.Headers;

namespace AI_Chat
{
    public sealed class Program
    {
        static void Main(string[] args)
        {
            //コンソールアプリケーションからAsyncを呼び出す
            Task task = MainAsync();
            //終了を待つ
            task.Wait();
        }

        private static VoiceNextConnection? VoiceCon; //Voiceチャットコネクション

        public static async Task MainAsync()
        {
            bool Op = true;             //本番フラグ
            bool msg = false;           //メッセージ表示
            uint intContextSize = 8192; //コンテクスト長さ
            uint silenceTimerCnt = 0;   //寝るTimerカウント
            // LLMモデルの場所
            string strModelPath = Environment.GetEnvironmentVariable("LLMPATH", System.EnvironmentVariableTarget.User) + @"dahara1\gemma-2-27b-it-gguf-japanese-imatrix\gemma-2-27b-it.f16.Q5_k_m.gguf";
            //チャットデータベース
            string strChatlogPath = Environment.GetEnvironmentVariable("CHATDB", System.EnvironmentVariableTarget.User) + @"ChatDB.db";
            //Awake、Asleep設定
            const uint itrvl = 10000;        // 10秒
            const uint silenceTimerMax = 30; //寝待ち長さ
            string[] AwakeWord = App.Default.AwakeKeyWord.Split(",");
            string[] AsleepWord = App.Default.AsleepKeyWord.Split(",");
            System.Timers.Timer? silenceTimer;

            try
            {
                //チャットログ要約処理
                Task task = HistorySummary.Run(strChatlogPath, intContextSize / 2, App.Default.ChatDB_Table);
                task.Wait();
                //チャットログシステム
                ChatHistoryDB chtDB;
                //AI待ちフラグ
                bool Wt = true;
                //LLMモデルのロードとパラメータの設定
                Console.ForegroundColor = ConsoleColor.Blue;
                ModelParams modPara = new(strModelPath)
                {
                    ContextSize = intContextSize,
                    GpuLayerCount = 60,
                };
                using LLamaWeights llmWeit = LLamaWeights.LoadFromFile(modPara);
                using LLamaContext llmContx = llmWeit.CreateContext(modPara);
                InteractiveExecutor itrEx = new(llmContx);
                //チャットログを読み込みます。
                ChatHistory chtHis = new ChatHistory();
                chtDB = new ChatHistoryDB(strChatlogPath, chtHis, App.Default.ChatDB_Table);
                ChatSession chtSess = new(itrEx, chtHis);
                var varHidewd = new LLamaTransforms.KeywordTextOutputStreamTransform(["User: ", "Assistant: "]);
                chtSess.WithOutputTransform(varHidewd);
                InferenceParams infPara = new()
                {
                    SamplingPipeline = new DefaultSamplingPipeline()
                    {
                        Temperature = App.Default.tmp,
                        Seed = 1337,
                    },
                    AntiPrompts = ["User:"],
                    MaxTokens = 256,
                };
                //タイマー定義
                silenceTimer = new System.Timers.Timer(itrvl);
                //チャットロジック
                Func<string, Task> Chat = async (string strUserInput) =>
                {
                    if (!Wt)
                    {
                        Wt = true;
                        ChatHistory.Message msgText = new(AuthorRole.User, strUserInput);
                        Console.ForegroundColor = ConsoleColor.White;
                        if (msg) Console.WriteLine("User: " + strUserInput);
                        if (Op) chtDB.WriteHistory(AuthorRole.User, "User: "+ strUserInput);
                        // 回答の表示
                        Console.ForegroundColor = ConsoleColor.Yellow;
                        string strMsg = "";
                        await foreach (string strText in chtSess.ChatAsync(msgText, infPara)) strMsg += strText;
                        //発信するときは「User:」や「Assistant:」を抜く
                        string strSndmsg = strMsg.Replace("User:", "").Replace("Assistant:", "").Replace("assistant:", "").Trim();
                        if (msg) Console.WriteLine("Assistant: " + strSndmsg);
                        if (Op) chtDB.WriteHistory(AuthorRole.Assistant, strMsg);
                        //音声ソフト実行
                        SpeechSynthesis(strSndmsg);
                        Thread.Sleep(3000);
                        Wt = false;
                    }
                };
                // SpeechRecognitionの設定
                //AI会話設定とイベント定義
                SpeechRecognizer recognizer = new SpeechRecognizer();
                // Set timeout settings.
                recognizer.Timeouts.InitialSilenceTimeout = TimeSpan.FromSeconds(6.0); // (認識結果が生成されるまでの) 無音を検出し、音声入力が続かないと見なす時間の長さ。
                recognizer.Timeouts.BabbleTimeout = TimeSpan.FromSeconds(4.0);         //認識できないサウンド (雑音) のリッスンを継続し、音声入力が終了したと見なし、認識処理を終了するまでの時間の長さ。
                recognizer.Timeouts.EndSilenceTimeout = TimeSpan.FromSeconds(1.2);     //(認識結果が生成された後の) 無音を検出し、音声入力が終了したと見なす時間の長さ。
                await recognizer.CompileConstraintsAsync();
                recognizer.ContinuousRecognitionSession.ResultGenerated += async (sender, e) =>
                {
                    //Asleep設定とイベント定義
                    bool Ap = false;
                    for (int i = 0; i < AsleepWord.Length; i++) if (Regex.IsMatch(e.Result.Text, $"^*{AsleepWord[i]}*$", RegexOptions.Singleline)) Ap = true;
                    if (Ap)
                    {
                        if (msg) Console.WriteLine($"{e.Result.Text}と言われました");
                        Wt = true;
                        silenceTimerCnt = 0;
                        silenceTimer.Stop();
                    }
                    //AI会話
                    if (!Wt)
                    {
                        silenceTimerCnt = 0;
                        silenceTimer.Stop();
                        await Chat(e.Result.Text.Replace("MK", "マンコ").Replace("TK", "チンコ").Replace("TP", "チンポ"));
                        silenceTimer.Start();
                    }
                    //Awake設定とイベント定義
                    bool Aw = false;
                    for (int i = 0; i < AwakeWord.Length; i++) if (Regex.IsMatch(e.Result.Text, $"^*{AwakeWord[i]}*$", RegexOptions.Singleline)) Aw = true;
                    if (Aw)
                    {
                        if (msg) Console.WriteLine($"{e.Result.Text}と言われました");
                        Wt = false;
                        //AIから話しかける
                        await Chat("(_ _)");
                        silenceTimerCnt = 0;
                        silenceTimer.Stop();
                        silenceTimer.Start();
                    }
                };
                //タイマーイベント定義
                silenceTimer.Elapsed  += async (sender, e) =>
                {
                    silenceTimerCnt++;
                    //AIから話しかける仕組み
                    if (silenceTimerCnt == 3)
                    {
                        Random rTalk = new Random();
                        if (rTalk.Next(0, 1) == 0) //乱数で0が出たら話しかける。トリガーは"??"
                        {
                            await Chat("??");
                        }
                    }
                    if (silenceTimerCnt >= silenceTimerMax)
                    {
                        Wt = true;
                        if (msg) Console.WriteLine("休止しました。");
                        silenceTimerCnt = 0;
                        silenceTimer.Stop();
                    }
                    else
                    {
                        if (recognizer.State == SpeechRecognizerState.Idle)
                        {
                            await recognizer.ContinuousRecognitionSession.StartAsync();
                        }
                    }
                };
                //AI会話タイムアウト
                recognizer.ContinuousRecognitionSession.Completed += async (sender, e) =>
                {
                    // Recognizer Restart
                    await recognizer.ContinuousRecognitionSession.StartAsync();
                };
                // Recognizer Start  
                await recognizer.ContinuousRecognitionSession.StartAsync();

                //★★★★★★★ここからDiscordの処理★★★★★★★★
                // ユーザー環境変数に登録した「DISCORD_TOKEN」を持ってくる
                string? token = Environment.GetEnvironmentVariable("DISCORD_TOKEN", System.EnvironmentVariableTarget.User);
                if (string.IsNullOrWhiteSpace(token))
                {
                    Console.WriteLine("Please specify a token in the DISCORD_TOKEN environment variable.");
                    Environment.Exit(1);
                    return;
                }
                DiscordConfiguration config = new()
                {
                    Token = token,
                    TokenType = TokenType.Bot,
                    Intents = DiscordIntents.AllUnprivileged | DiscordIntents.MessageContents | DiscordIntents.GuildVoiceStates
                };
                DiscordClient client = new(config);
                client.UseVoiceNext();

                //メッセージイベント
                client.MessageCreated += async (client, eventArgs) =>
                {
                    //ボットに応答しない
                    if (eventArgs.Message.Author.Id==client.CurrentUser.Id) return;
                    //ターゲットチャンネル
                    if (eventArgs.Channel.Id != App.Default.Channel_Id) return;
                    // ユーザーのターン
                    ChatHistory.Message msg = new(AuthorRole.User, "User: " + eventArgs.Message.Content);
                    if (Op) chtDB.WriteHistory(AuthorRole.User, "User: " +  eventArgs.Message.Content);
                    // AIのターン
                    string strMsg = "";
                    await foreach (string strAns in chtSess.ChatAsync(msg, infPara))
                    {
                        strMsg += strAns;
                    }
                    //Discordに発信するときは「User:」や「Assistant:」を抜く
                    string strSndmsg = strMsg.Replace("User:", "").Replace("Assistant:", "").Replace("assistant:", "").Trim();
                    await eventArgs.Message.RespondAsync(strSndmsg);
                    if (Op) chtDB.WriteHistory(AuthorRole.Assistant, strMsg.Replace("User:", "").Trim()+"\n"+"User:");
                    if (VoiceCon is not null) //Voiceチャンネルが開いているときは音声を送信
                    {
                        MemoryStream msRes = SpeechSynthesis(strSndmsg, false);
                        await SendAsync(VoiceCon, msRes);
                    }
                };
                client.Ready += OnClientReady;
                client.GuildAvailable += OnGuildAvailable;
                client.VoiceStateUpdated += OnVoiceStateUpdated;

                DiscordActivity status = new("with fire", ActivityType.Playing);
                await client.ConnectAsync(status, UserStatus.Online);

                await Task.Delay(-1);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }

        private static Task OnClientReady(DiscordClient client, ReadyEventArgs e)
        {
            Console.WriteLine("Bot is ready!");
            return Task.CompletedTask;
        }

        private static Task OnGuildAvailable(DiscordClient client, GuildCreateEventArgs e)
        {
            Console.WriteLine($"Guild available: {e.Guild.Name}");
            return Task.CompletedTask;
        }

        private static async Task OnVoiceStateUpdated(DiscordClient client, VoiceStateUpdateEventArgs e)
        {
            try
            {
                //Bot以外のチャンネルが開いた場合
                if (e.After.Channel != null && !e.After.User.IsBot)
                {
                    Console.WriteLine($"{e.After.User.Username} joined voice channel {e.After.Channel.Name}");

                    var vnext = client.GetVoiceNext();
                    var connection = await vnext.ConnectAsync(e.After.Channel);

                    VoiceCon = connection;

                }
                //Botが退出した場合はコネクションをNULLにする
                if (e.After.Channel == null && e.After.User.IsBot)
                {
                    VoiceCon = null;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
        private static async Task SendAsync(VoiceNextConnection connection, MemoryStream ms)
        {
            try
            {
                var transmit = connection.GetTransmitSink();
                var pcm = ConvertAudioToPcm(ms);
                await pcm.CopyToAsync(transmit);
                await pcm.DisposeAsync();
                ms.Close();
                ms.Dispose();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
        private static Stream ConvertAudioToPcm(MemoryStream ms)
        {
            try
            {
                var ffmpeg = Process.Start(new ProcessStartInfo
                {
                    FileName = "ffmpeg",
                    Arguments = $@"-i pipe:0 -ac 2 -f s16le -ar 48000 pipe:1",
                    RedirectStandardInput = true,
                    RedirectStandardOutput = true,
                    UseShellExecute = false
                });

                ffmpeg.StandardInput.BaseStream.WriteAsync(ms.ToArray(), 0, (int)ms.Length);
                ffmpeg.StandardInput.BaseStream.FlushAsync();
                return ffmpeg.StandardOutput.BaseStream;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return null;
            }
        }

        private static MemoryStream SpeechSynthesis(string strMsg, bool booPlay = true)
        {
            //音声ソフト選択
            if (App.Default.SpeechSynth==1) return CevioAI(strMsg, booPlay);
            if (App.Default.SpeechSynth==2)
            {
                if (Process.GetProcessesByName("VOICEVOX").Length == 0)
                {
                    var vx = new ProcessStartInfo();
                    vx.FileName = Environment.GetEnvironmentVariable("VOICEVOX_PATH", System.EnvironmentVariableTarget.User);
                    vx.UseShellExecute = true;
                    Process.Start(vx);
                    Thread.Sleep(2000);
                }
                ValueTask<MemoryStream> msRes = Voicevox(strMsg);
                msRes.AsTask().Wait();
                if(booPlay)
                {
                    var player = new SoundPlayer(msRes.Result);
                    player.PlaySync();
                    msRes.Result.Close();
                }
                else
                {
                    return msRes.Result;
                }
            }
            return null;
        }
        private static MemoryStream CevioAI(string strMsg, bool booPlay)
        {
            try
            {
                MemoryStream ms = new MemoryStream();
                string strWavPath = Environment.GetEnvironmentVariable("TESTDATA", System.EnvironmentVariableTarget.User) + "output.wav";

                dynamic service = Activator.CreateInstance(Type.GetTypeFromProgID("CeVIO.Talk.RemoteService2.ServiceControl2V40"));
                service.StartHost(false);
                dynamic talker = Activator.CreateInstance(Type.GetTypeFromProgID("CeVIO.Talk.RemoteService2.Talker2V40"));
                talker.Cast = App.Default.CEVIOAI_CAST;
                if (booPlay)
                {
                    dynamic result = talker.Speak(strMsg);
                    result.Wait();
                }
                else
                {
                    dynamic result = talker.OutputWaveToFile(strMsg, strWavPath);
                    using (FileStream file = new FileStream(strWavPath, FileMode.Open, FileAccess.Read))
                        file.CopyTo(ms);
                }

                //開放忘れるとメモリリーク
                System.Runtime.InteropServices.Marshal.ReleaseComObject(talker);
                System.Runtime.InteropServices.Marshal.ReleaseComObject(service);
                return ms;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return null;
            }
        }
        private static async ValueTask<MemoryStream> Voicevox(string strMsg)
        {
            MemoryStream? ms;
            try
            {
                using (var httpClient = new HttpClient())
                {
                    string strQuery;
                    // 音声クエリを生成
                    using (var varRequest = new HttpRequestMessage(new HttpMethod("POST"), $"http://127.0.0.1:{App.Default.VOICEVOX_PORT}/audio_query?text={strMsg}&speaker={App.Default.VOICEVOX_SPEAKER}&speedScale=1.5&prePhonemeLength=0&postPhonemeLength=0&intonationScale=1.16&enable_interrogative_upspeak=true"))
                    {
                        varRequest.Headers.TryAddWithoutValidation("accept", "application/json");
                        varRequest.Content = new StringContent("");
                        varRequest.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/x-www-form-urlencoded");
                        var response = await httpClient.SendAsync(varRequest);
                        strQuery = response.Content.ReadAsStringAsync().Result;
                    }

                    // 音声クエリから音声合成
                    using (var request = new HttpRequestMessage(new HttpMethod("POST"), $"http://127.0.0.1:{App.Default.VOICEVOX_PORT}/synthesis?speaker={App.Default.VOICEVOX_SPEAKER}&enable_interrogative_upspeak=true&speedScale=1.5&prePhonemeLength=0&postPhonemeLength=0&intonationScale=1.16"))
                    {
                        request.Headers.TryAddWithoutValidation("accept", "audio/wav");
                        request.Content = new StringContent(strQuery);
                        request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
                        var response = await httpClient.SendAsync(request);
                        // 音声を保存
                        using (ms = new MemoryStream())
                        {
                            using (var httpStream = await response.Content.ReadAsStreamAsync())
                            {
                                httpStream.CopyTo(ms);
                                ms.Flush();
                            }
                        }
                    }
                }
                ms = new MemoryStream(ms.ToArray());
                return ms;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                return null;
            }
        }


    }
}




ChatHistoryDB.cs SQLiteでチャット履歴を管理
1.8音声のみからの修正点
・2024/10/26 Assistantの性格セットアップ行追加で2行目にcmd.ExecuteNonQuery()がなかったので追加した。

using LLama.Common;
using System.Data.SQLite;
using HtmlAgilityPack;

namespace AI_Chat
{
    class ChatHistoryDB
    {
        ChatHistory? chtHis;
        string strDbpath;
        Dictionary<string, AuthorRole>? Roles = new Dictionary<string, AuthorRole> { { "System", AuthorRole.System }, { "User", AuthorRole.User }, { "Assistant", AuthorRole.Assistant } };
        string strTable;

        public ChatHistoryDB(string strDbpath, ChatHistory chtHis, string strTable)
        {

            this.chtHis= chtHis;
            this.strDbpath= strDbpath;
            this.strTable= strTable;

            try
            {
                var conSb = new SQLiteConnectionStringBuilder { DataSource = strDbpath };

                var con = new SQLiteConnection(conSb.ToString());
                con.Open();

                using (var cmd = new SQLiteCommand(con))
                {
                    cmd.CommandText = $"CREATE TABLE IF NOT EXISTS {strTable}(" +
                        "\"sq\"  INTEGER," +
                        "\"dt\"  TEXT NOT NULL," +
                        "\"id\"  TEXT NOT NULL," +
                        "\"msg\" TEXT," +
                        "\"flg\" INTEGER DEFAULT 0, PRIMARY KEY(\"sq\"))";
                    cmd.ExecuteNonQuery();

                    cmd.CommandText = $"select count(*)  from {strTable}";
                    using (var reader = cmd.ExecuteReader())
                    {
                        //一行も存在しない場合はシステム行をセットアップ
                        long reccount = 0;
                        if (reader.Read()) reccount = (long)reader[0];
                        reader.Close();
                        if (reccount<1)
                        {
                            //Assistantの性格セットアップ行追加
                            cmd.CommandText = $"insert into {strTable}(dt,id,msg) values(datetime('now', 'localtime'),'System','あなたは優秀なアシスタントです。')";
                            cmd.ExecuteNonQuery();
                            //要約行追加
                            cmd.CommandText = $"insert into {strTable}(dt,id,msg) values(datetime('now', 'localtime'),'System','')";
                            cmd.ExecuteNonQuery();
                            //最新情報行追加
                            cmd.CommandText = $"insert into {strTable}(dt,id,msg) values(datetime('now', 'localtime'),'System','')";
                            cmd.ExecuteNonQuery();
                        }
                    }
                    //最新情報を更新
                    cmd.CommandText = $"update {strTable} set dt=datetime('now', 'localtime'),msg='今日は、{DateTime.Now.ToString("yyyy年M月d日dddd")}です。\n" + 
                                       GetWether().Result + "\n" + GetNikkeiStkAvg().Result + "' where sq=3";
                    cmd.ExecuteNonQuery();

                    //システム行を履歴に読み込む"
                    string strMsg = "";
                    cmd.CommandText = $"select * from {strTable} where id='System' order by sq";
                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            strMsg += (string)reader["msg"] + "\n";
                        }
                    }
                    if (chtHis is null) chtHis=new ChatHistory();
                    chtHis.AddMessage(AuthorRole.System,strMsg);

                    //会話行を履歴に読み込む
                    cmd.CommandText = $"select * from {strTable} where flg=0 and id<>'System' order by sq";
                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            chtHis.AddMessage(Roles[(string)reader["id"]], (string)reader["msg"]);
                        }
                    }
                }

                con.Close();

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

        }

        public interface IDisposable
        {
            void Dispose();
        }


        public void WriteHistory(AuthorRole aurID, string strMsg, bool booHis = false)
        {
            try
            {
                var conSb = new SQLiteConnectionStringBuilder { DataSource = strDbpath };

                var con = new SQLiteConnection(conSb.ToString());
                con.Open();

                using (var cmd = new SQLiteCommand(con))
                {
                    if (booHis)
                    {
                        if (chtHis is null) chtHis=new ChatHistory();
                        chtHis.AddMessage(aurID, strMsg);
                    }
                    cmd.CommandText = $"insert into {strTable}(dt,id,msg) values(datetime('now', 'localtime'),'{Roles.FirstOrDefault(v => v.Value.Equals(aurID)).Key}','{strMsg}')";
                    cmd.ExecuteNonQuery();
                }
                con.Close();

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

        private static async Task<string> GetNikkeiStkAvg()
        {
            // 株価取得 
            HttpClient webClient = new HttpClient();
            string page = await webClient.GetStringAsync("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 async Task<string> GetWether()
        {
            // 天気予報 
            HttpClient webClient = new HttpClient();
            string page = await webClient.GetStringAsync("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();
                }
                txtcontents += "」です。";
            }
            //Console.WriteLine(txtcontents);
            return txtcontents;
        }

    }
}




HistorySummary.cs 長くなった会話を要約して圧縮しSEQ=3のシステム行のmsgにぶちこむ。要約が終わった行は論理削除。
要約には「DataPilot-ArrowPro-7B-RobinHood」が優秀だったので使ってます。
1.8音声のみからの修正点
・2024/10/28 要約行をSeq=2に修正

using LLama.Common;
using LLama;
using System.Data.SQLite;

namespace AI_Chat
{
    internal class HistorySummary
    {
        public static async Task Run(string strChatlogPath, uint HistoryMax, string strTable)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("**Start History Summary**");

            try
            {
                // LLMモデルの場所
                string strModelPath = Environment.GetEnvironmentVariable("LLMPATH", System.EnvironmentVariableTarget.User) + @"mmnga\DataPilot-ArrowPro-7B-RobinHood-gguf\DataPilot-ArrowPro-7B-RobinHood-Q8_0.gguf";
                // ChatDBから行を読み込む
                string strChtHis = "";
                uint i = 0;
                uint intCtxtSize = 4096; //一度に処理できる文字数
                long svSq = 0;    //処理を開始したSq
                long svSqMax = 0; //処理を終了したSq
                var conSb = new SQLiteConnectionStringBuilder { DataSource = strChatlogPath };
                var con = new SQLiteConnection(conSb.ToString());
                con.Open();
                using (var cmd = new SQLiteCommand(con))
                {
                    //有効文字数をカウント
                    cmd.CommandText = $"select sum(length(msg))  from {strTable} where flg=0";
                    using (var reader = cmd.ExecuteReader())
                    {
                        reader.Read();
                        //Console.WriteLine($"文字数:{reader[0]}");
                        //チャット履歴の最大行に満たないか、最大文字数を越えても処理できる文字数が充分でない場合は処理をしない
                        if ((long)reader[0] < (long)HistoryMax || (long)reader[0] - (long)HistoryMax < (long)intCtxtSize)
                        {
                            con.Close();
                            Console.ForegroundColor = ConsoleColor.Green;
                            Console.WriteLine("**E.N.D History Summary**");
                            return;
                        }
                    }
                    //有効行を呼んで要約する
                    cmd.CommandText = $"select * from {strTable} where flg=0 and sq>2 order by sq";
                    string strCht = "";
                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            strCht = (string)reader["id"] + ": " + ((string)reader["msg"]).Replace("User:", "").Replace("Assistant:", "").Replace("assistant:", "").Trim() + "\n";
                            i += (uint)strCht.Length;
                            if (i > intCtxtSize) { break; }
                            strChtHis += strCht;
                            //システム行以外で処理を開始した行を記録
                            if ((string)reader["id"] != "System" && svSq == 0)
                            {
                                svSq = (long)reader["sq"];
                            }
                            svSqMax = (long)reader["sq"];
                        }
                    }
                }

                //LLMの処理
                Console.ForegroundColor = ConsoleColor.Blue;

                //LLMモデルのロードとパラメータの設定
                ModelParams modPara = new(strModelPath)
                {
                    ContextSize = intCtxtSize,
                    GpuLayerCount = 60
                };

                ChatHistory chtHis = new ChatHistory();
                chtHis.AddMessage(AuthorRole.System, "#命令書\n" +
                                                    "・あなたは優秀な編集者です。\n" +
                                                    "・ユーザーとアシスタントの会話を要約してください。\n" +
                                                    "#条件\n" +
                                                    "・重要なキーワードを取りこぼさない。\n" +
                                                    "#出力形式\n" +
                                                    "・例)要約: ふたりは親密な会話しました。");
                using LLamaWeights llmWeit = LLamaWeights.LoadFromFile(modPara);
                using LLamaContext llmContx = llmWeit.CreateContext(modPara);
                InteractiveExecutor itrEx = new(llmContx);
                ChatSession chtSess = new(itrEx, chtHis);
                var varHidewd = new LLamaTransforms.KeywordTextOutputStreamTransform(["User:", "Assistant:"]);
                chtSess.WithOutputTransform(varHidewd);
                InferenceParams infPara = new()
                {
                    Temperature = 0f,
                    AntiPrompts = new List<string> { "User:" }
                };

                // ユーザーのターン
                Console.ForegroundColor = ConsoleColor.White;
                string strInput =  strChtHis;
                ChatHistory.Message msg = new(AuthorRole.User, strInput);

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

                using (var cmd = new SQLiteCommand(con))
                {
                    //sq=1に要約内容を更新する(要約という文字、改行、空白を削除)
                    string strSql = $"update {strTable} set msg= '{strMsg.Replace("要約:", "").Replace("\r", "").Replace("\n", "").Trim()}' where sq=2";
                    //Console.Write (strSql+"\n");
                    cmd.CommandText = strSql;
                    cmd.ExecuteNonQuery();

                    //処理した行を論理削除する
                    strSql = $"update {strTable} set flg=1 where (sq between {svSq} and {svSqMax}) and id<>'System' and flg=0";
                    //Console.Write(strSql+"\n");
                    cmd.CommandText = strSql;
                    cmd.ExecuteNonQuery();
                }

                //DBクローズ
                con.Close();

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

            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("**E.N.D History Summary**");

        }
    }
}



ずっと立ち上げるならスケジューラで再起動すると要約が走るのでいいかも
PowerShellコマンドの参考例
AI_Server01.ps1

stop-process -Name "アプリのプロセス名"
Start-Sleep -Seconds 3
Start-Process -FilePath "アプリパス+EXE名"

Follow me!

コメントを残す

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