minecraftからローカルLLMとチャットする c#プログラム②
モンハンワイルズやりすぎて記事アップをさぼっておりました。今回のプログラムはLLM Doghouseのデータベースを横取りしてminecraftからチャットするプログラムです。一応AIはしゃべります。まだ試作なのでデータベースにチャット履歴の書き込みは行っておりません。
設定はLLM Doghouseから行ってください。
Nuget情報

minecraft serverを立ち上げてから実行してください。
データベースパスは、LLM Doghouseの実行ファイルがあるディレクトリを指定してください。
intSeqは、チャットNOを設定してください。
strPath、strPassは、ご自身の環境に合わせて設定してください。
Program.cs
using LLama.Common;
using LLama;
using LLama.Sampling;
using System.Text.RegularExpressions;
using MinecraftConnection;
using LLM_Doghouse;
using System.Data.SQLite;
using System.Media;
using System.Net.Http.Headers;
namespace ChatProgram
{
public class Program
{
static private string strSavline = "";
static private bool booSkip = true;
static private ChatHistoryDB? chtDB;
static private string DatabasePath = Environment.GetEnvironmentVariable("LDGPATH", System.EnvironmentVariableTarget.User) + @"\ChatDB.db";
private const int intSeq = 7;
static private string strPath = "";
static private string strTable = $"ch{intSeq}";
static private string strSys = "";
static private string strSysOp = "";
static private string strLng = "";
static private HistorySummary? sum;
static private uint intCtx = 0;
static private float fltTmp = 0f;
static private int intGpu = 0;
static private string[]? strAnt;
static private float fltFrp = 0f;
static private int intSyt = 0;
static private int intVvx = 0;
static private string strCvo = "";
static private int intPort = 50021;
static void Main(string[] args)
{
//コンソールアプリケーションからAsyncを呼び出す大元はTaskを使用する
Task task = MainAsync();
//終了を待つ
task.Wait();
}
public static async Task MainAsync()
{
//初期処理
Console.ForegroundColor = ConsoleColor.Green;
sum = new(DatabasePath);
InitSettingEd();
InitSettingSys();
InitSettingCom();
//チャット履歴サマリ
await sum.Run(intCtx, strTable);
sum = null;
//LLMモデルのロードとパラメータの設定
ModelParams modPara = new(strPath)
{
ContextSize = intCtx,
GpuLayerCount = intGpu
};
using LLamaWeights llmWeit = LLamaWeights.LoadFromFile(modPara);
using LLamaContext llmContx = llmWeit.CreateContext(modPara);
InteractiveExecutor itrEx = new(llmContx);
//System Prompt+チャットの方向づけ
ChatHistory chtHis;
chtHis = new ChatHistory();
ChatHistoryDB chtDB = new(DatabasePath, chtHis, strTable, strSys, strSysOp, strLng);
ChatSession chtSess = new(itrEx, chtHis);
var varHidewd = new LLamaTransforms.KeywordTextOutputStreamTransform(["User:", "Assistant:"]);
chtSess.WithOutputTransform(varHidewd);
InferenceParams infPara = new()
{
SamplingPipeline = new DefaultSamplingPipeline()
{
Temperature = fltTmp,
Seed = 1337,
FrequencyPenalty = fltFrp,
},
AntiPrompts = strAnt,
};
string strAddress = "127.0.0.1";
ushort shtPort = 25575;
string strPass = "doghouse";
MinecraftCommands MinCmd = new MinecraftCommands(strAddress, shtPort, strPass);
string strline = "";
while (true)
{
strline = ReadLog();
if (strline != null)
{
if (strline.IndexOf(@"<")>0)
{
Match rgxMtc = Regex.Match(strline, @"<.*> (.*)");
Group rgxGrp = rgxMtc.Groups[1];
CaptureCollection colCap = rgxGrp.Captures;
// ユーザーのターン
Console.ForegroundColor = ConsoleColor.White;
Console.Write("\nUser: ");
string strInput = colCap[0].Value;
Console.Write(strInput);
ChatHistory.Message msg = new(AuthorRole.User, "User: " + strInput);
// AIのターン
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("\nAssistant: ");
string strMsg = "";
await foreach (string strAns in chtSess.ChatAsync(msg, infPara))
{
Console.Write(strAns);
strMsg += strAns;
}
MinCmd.DisplayMessage(strMsg.Replace("User:",""));
SpeechSynthesis(strMsg.Replace("User:", ""));
}
}
Thread.Sleep(3000);
}
}
static private string ReadLog()
{
string strline = "";
string strPath = @"E:\MinecraftServer\logs\latest.log";
using (FileStream fs = new FileStream(strPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (StreamReader sr = new StreamReader(fs))
{
//最初は最終行まで、その後は読み込んだところまで
strline = sr.ReadLine();
while (true)
{
if (sr.Peek() < 0 || !booSkip)
{
if (strSavline == strline)
strline = "";
else
strSavline = strline;
booSkip = true;
break;
}
if (strline == strSavline)
{
booSkip = false;
}
strline = sr.ReadLine();
}
return (strline);
}
}
}
static private void InitSettingEd()
{
try
{
var conSb = new SQLiteConnectionStringBuilder { DataSource = DatabasePath };
var con = new SQLiteConnection(conSb.ToString());
con.Open();
using (var cmd = new SQLiteCommand(con))
{
cmd.CommandText = $"select * from mng where no={intSeq}";
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
strSys = reader["sys"].ToString();
intCtx = uint.Parse(reader["ctx"].ToString());
fltTmp = float.Parse(reader["tmp"].ToString());
intGpu = int.Parse(reader["gpu"].ToString());
strPath = reader["mdl"].ToString();
strAnt = reader["ant"].ToString().Split(",");
fltFrp = float.Parse(reader["frp"].ToString());
intSyt = int.Parse(reader["syt"].ToString());
intVvx = int.Parse(reader["vvx"].ToString());
strCvo = reader["cvo"].ToString();
}
}
}
con.Close();
}
catch (Exception ex)
{
//MessageBox.Show(ex.Message, this.Title, MessageBoxButton.OK, MessageBoxImage.Error);
Console.WriteLine(ex.Message);
}
}
static private void InitSettingSys()
{
try
{
var conSb = new SQLiteConnectionStringBuilder { DataSource = DatabasePath };
var con = new SQLiteConnection(conSb.ToString());
con.Open();
using (var cmd = new SQLiteCommand(con))
{
cmd.CommandText = $"select * from sum where no=1";
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
sum.SystemPrompt = reader["sys"].ToString();
sum.ModelPath = reader["mdl"].ToString();
sum.ContextSize = uint.Parse(reader["ctx"].ToString());
sum.GpuLayerCount = int.Parse(reader["gpu"].ToString());
sum.Temperature = float.Parse(reader["tmp"].ToString());
sum.AntiPrompt = reader["ant"].ToString();
}
reader.Close();
}
}
con.Close();
}
catch (Exception ex)
{
//MessageBox.Show(ex.Message, this.Title, MessageBoxButton.OK, MessageBoxImage.Error);
Console.WriteLine(ex.Message);
}
}
static private void InitSettingCom()
{
try
{
var conSb = new SQLiteConnectionStringBuilder { DataSource = DatabasePath };
var con = new SQLiteConnection(conSb.ToString());
con.Open();
using (var cmd = new SQLiteCommand(con))
{
cmd.CommandText = $"select * from com where no=1";
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
strSysOp = reader["sys"].ToString();
strLng = reader["lng"].ToString();
}
reader.Close();
}
}
con.Close();
}
catch (Exception ex)
{
//MessageBox.Show(ex.Message, this.Title, MessageBoxButton.OK, MessageBoxImage.Error);
Console.WriteLine(ex.Message);
}
}
private static void SpeechSynthesis(string strMsg)
{
//音声ソフト選択
if (intSyt==1) CevioAI(strMsg);
if (intSyt==2)
{
Task task = Voicevox(strMsg);
task.Wait();
}
}
private static void CevioAI(string strMsg)
{
try
{
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 = strCvo;
dynamic result = talker.Speak(strMsg);
result.Wait();
//開放忘れるとメモリリーク
System.Runtime.InteropServices.Marshal.ReleaseComObject(talker);
System.Runtime.InteropServices.Marshal.ReleaseComObject(service);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
private static async Task 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:{intPort}/audio_query?text={strMsg}&speaker={intVvx}&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:{intPort}/synthesis?speaker={intVvx}&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());
//読み込む
var player = new SoundPlayer(ms);
//再生する
player.PlaySync();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
}
チャット履歴をデータベースから読み込む
ChatHistoryDB.cs
using LLama.Common;
using System.Data.SQLite;
using System.Globalization;
namespace LLM_Doghouse
{
class ChatHistoryDB
{
ChatHistory? chtHis;
string DatabasePath;
Dictionary<string, AuthorRole>? Roles = new Dictionary<string, AuthorRole> { { "System", AuthorRole.System }, { "User", AuthorRole.User }, { "Assistant", AuthorRole.Assistant } };
string strTable;
public ChatHistoryDB(string DatabasePath, ChatHistory chtHis, string strTable, string strSys, string strSysOp, string strLng)
{
this.chtHis= chtHis;
this.DatabasePath= DatabasePath;
this.strTable= strTable;
try
{
var conSb = new SQLiteConnectionStringBuilder { DataSource = DatabasePath };
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='{strSys}' where sq=1";
cmd.ExecuteNonQuery();
cmd.CommandText = $"update {strTable} set dt=datetime('now', 'localtime'),msg='{strSysOp}\nToday:{DateTime.Now.ToString("d",CultureInfo.CreateSpecificCulture(strLng))}' 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)
{
//MessageBox.Show(ex.Message, "ChatHistoryDB", MessageBoxButton.OK, MessageBoxImage.Error);
Console.WriteLine(ex.Message);
}
}
public interface IDisposable
{
void Dispose();
}
public long WriteHistory(AuthorRole aurID, string strMsg, bool booHis = false)
{
long lngSq = 0;
try
{
var conSb = new SQLiteConnectionStringBuilder { DataSource = DatabasePath };
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();
//追加した行のSEQを返す
cmd.CommandText = $"select max(sq) sq from {strTable}";
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
if(!long.TryParse(reader["sq"].ToString(), out lngSq))
lngSq = 0;
}
}
}
con.Close();
return lngSq;
}
catch (Exception ex)
{
//MessageBox.Show(ex.Message, "ChatHistoryDB", MessageBoxButton.OK, MessageBoxImage.Error);
Console.WriteLine(ex.Message);
return lngSq;
}
}
}
}
履歴が多いときに要約するプログラム
HistorySummary.cs
using LLama.Common;
using LLama;
using System.Data.SQLite;
using LLama.Sampling;
using System.Windows;
namespace LLM_Doghouse
{
internal class HistorySummary
{
private string strSys = "";
private uint intCtx = 0;
private float fltTmp = 0;
private int intGpu = 0;
private string strMdl = "";
private string strAnt = "";
public string SystemPrompt
{
get => strSys;
set => strSys = value;
}
public uint ContextSize
{
get => intCtx;
set => intCtx = value;
}
public int GpuLayerCount
{
get => intGpu;
set => intGpu = value;
}
public float Temperature
{
get => fltTmp;
set => fltTmp = value;
}
public string ModelPath
{
get => strMdl;
set => strMdl = value;
}
public string AntiPrompt
{
get => strAnt;
set => strAnt = value;
}
private string DatabasePath = "";
public HistorySummary(string DatabasePath)
{
this.DatabasePath = DatabasePath;
}
public async Task<bool> Run(uint HistoryMax, string strTable)
{
try
{
string strChtHis = ""; // ChatDBから行を読み込む
long i = 0;
long svSq = 0; //処理を開始したSq
long svSqMax = 0; //処理を終了したSq
long lngTrc = 0;
string[] strAntA = strAnt.Split(",");
var conSb = new SQLiteConnectionStringBuilder { DataSource = DatabasePath };
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)intCtx)
{
con.Close();
return true;
}
lngTrc = (long)reader[0] - (long)HistoryMax;
if (lngTrc > (long)intCtx) lngTrc = (long)intCtx;
reader.Close();
}
//有効行を呼んで要約する
cmd.CommandText = $"select * from {strTable} where flg=0 and sq>3 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 += (long)strCht.Length;
if (i > lngTrc) { break; }
strChtHis += strCht;
//システム行以外で処理を開始した行を記録
if ((string)reader["id"] != "System" && svSq == 0)
{
svSq = (long)reader["sq"];
}
svSqMax = (long)reader["sq"];
}
reader.Close();
}
}
//LLMモデルのロードとパラメータの設定
ModelParams modPara = new(strMdl)
{
ContextSize = intCtx,
GpuLayerCount = intGpu
};
ChatHistory chtHis = new ChatHistory();
chtHis.AddMessage(AuthorRole.System, strSys);
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()
{
SamplingPipeline = new DefaultSamplingPipeline()
{
Temperature = fltTmp,
Seed = 1337,
},
AntiPrompts = strAntA,
};
// ユーザーのターン
string strInput = strChtHis;
ChatHistory.Message msg = new(AuthorRole.User, strInput);
// AIのターン
string strMsg = "";
await foreach (string strAns in chtSess.ChatAsync(msg, infPara))
{
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";
cmd.CommandText = strSql;
cmd.ExecuteNonQuery();
//処理した行を論理削除する
strSql = $"update {strTable} set flg=1 where (sq between {svSq} and {svSqMax}) and id<>'System' and flg=0";
cmd.CommandText = strSql;
cmd.ExecuteNonQuery();
}
//DBクローズ
con.Close();
return false;
}
catch (Exception ex)
{
string msg = ex.Message;
//MessageBox.Show(ex.Message, "HistorySummary", MessageBoxButton.OK, MessageBoxImage.Error);
return true;
}
}
}
}
実行結果
絵文字は表示できないようですが、ちゃんと小春六花の声でしゃべってくれました。
