mirror of
https://github.com/barelyprofessional/KfChatDotNet.git
synced 2026-05-02 04:22:04 -04:00
Migrated moods and prompts to the settings.
Removed the weird concurrent dictionary and replaced with Redis. Removed the cleanup watchdog in favor of Redis expiration
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Humanizer;
|
||||
using KfChatDotNetBot.Extensions;
|
||||
using KfChatDotNetBot.Models;
|
||||
using KfChatDotNetBot.Models.DbModels;
|
||||
@@ -36,70 +37,28 @@ public class NoraCommand : ICommand
|
||||
{
|
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private static readonly string? PromptFilePath = FindPromptFile();
|
||||
private static string? _cachedPrompt;
|
||||
private static DateTime _cachedPromptLastModified;
|
||||
|
||||
private static string? FindPromptFile()
|
||||
{
|
||||
// Check next to the executable first, then fall back to the Commands directory in the source tree
|
||||
var exeDir = AppContext.BaseDirectory;
|
||||
var candidate = Path.Combine(exeDir, "Commands", "NoraPrompt.txt");
|
||||
if (File.Exists(candidate)) return candidate;
|
||||
|
||||
candidate = Path.Combine(exeDir, "NoraPrompt.txt");
|
||||
if (File.Exists(candidate)) return candidate;
|
||||
|
||||
Logger.Error("NoraPrompt.txt not found in {exeDir}/Commands/ or {exeDir}/", exeDir);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? LoadPrompt()
|
||||
{
|
||||
if (PromptFilePath == null) return null;
|
||||
|
||||
var lastWrite = File.GetLastWriteTimeUtc(PromptFilePath);
|
||||
if (_cachedPrompt != null && lastWrite == _cachedPromptLastModified)
|
||||
return _cachedPrompt;
|
||||
|
||||
try
|
||||
{
|
||||
_cachedPrompt = File.ReadAllText(PromptFilePath).Trim();
|
||||
_cachedPromptLastModified = lastWrite;
|
||||
Logger.Info("Loaded Nora prompt from {path} ({length} chars)", PromptFilePath, _cachedPrompt.Length);
|
||||
return _cachedPrompt;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Failed to read NoraPrompt.txt");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Regex> Patterns => [
|
||||
new Regex(@"^nora\s+(?<message>.+)", RegexOptions.IgnoreCase)
|
||||
];
|
||||
|
||||
public string? HelpText => "Ask Nora AI a question (max 15 words, 140 chars). Use '!nora reset' to clear context.";
|
||||
public string HelpText => "Ask Nora AI a question (max 15 words, 140 chars). Use '!nora reset' to clear context.";
|
||||
|
||||
public UserRight RequiredRight => UserRight.Loser;
|
||||
|
||||
public TimeSpan Timeout => TimeSpan.FromSeconds(30);
|
||||
|
||||
public RateLimitOptionsModel? RateLimitOptions => new RateLimitOptionsModel
|
||||
public RateLimitOptionsModel RateLimitOptions => new()
|
||||
{
|
||||
Window = TimeSpan.FromMinutes(1),
|
||||
MaxInvocations = 3,
|
||||
Flags = RateLimitFlags.None
|
||||
};
|
||||
|
||||
private const int MaxWords = 30;
|
||||
private const int MaxCharacters = 300;
|
||||
|
||||
public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user,
|
||||
GroupCollection arguments, CancellationToken ctx)
|
||||
{
|
||||
var userMessage = arguments["message"].Value.Trim();
|
||||
var manager = new ConversationContextManager();
|
||||
|
||||
// Handle !nora reset — clear conversation context
|
||||
if (userMessage.Equals("reset", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -116,8 +75,8 @@ public class NoraCommand : ICommand
|
||||
return;
|
||||
}
|
||||
|
||||
var resetKey = ConversationContextManager.GetContextKey(mode, user.KfId, message.RoomId);
|
||||
var cleared = ConversationContextManager.ClearContext(resetKey);
|
||||
var resetKey = ConversationContextManager.GetContextKeyAsync(mode, user.KfId, message.RoomId);
|
||||
var cleared = await manager.ClearContextAsync(resetKey);
|
||||
await botInstance.SendChatMessageAsync(
|
||||
cleared
|
||||
? $"{user.FormatUsername()}, your conversation context has been cleared."
|
||||
@@ -127,22 +86,24 @@ public class NoraCommand : ICommand
|
||||
return;
|
||||
}
|
||||
|
||||
var maxWords = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.GrokNoraMaxWords)).ToType<int>();
|
||||
var maxCharacters = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.GrokNoraMaxCharacters)).ToType<int>();
|
||||
// Validate word count
|
||||
var wordCount = userMessage.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
|
||||
if (wordCount > MaxWords)
|
||||
if (wordCount > maxWords)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, your message has {wordCount} words. Maximum is {MaxWords} words.",
|
||||
$"{user.FormatUsername()}, your message has {wordCount} words. Maximum is {maxWords} words.",
|
||||
true,
|
||||
autoDeleteAfter: TimeSpan.FromSeconds(15));
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate character count
|
||||
if (userMessage.Length > MaxCharacters)
|
||||
if (userMessage.Length > maxCharacters)
|
||||
{
|
||||
await botInstance.SendChatMessageAsync(
|
||||
$"{user.FormatUsername()}, your message has {userMessage.Length} characters. Maximum is {MaxCharacters} characters.",
|
||||
$"{user.FormatUsername()}, your message has {userMessage.Length} characters. Maximum is {maxCharacters} characters.",
|
||||
true,
|
||||
autoDeleteAfter: TimeSpan.FromSeconds(15));
|
||||
return;
|
||||
@@ -177,7 +138,7 @@ public class NoraCommand : ICommand
|
||||
}
|
||||
|
||||
// Step 2: Build conversation context and get Grok AI response
|
||||
var basePrompt = LoadPrompt();
|
||||
var basePrompt = (await SettingsProvider.GetValueAsync(BuiltIn.Keys.GrokNoraPrompt)).Value;
|
||||
if (basePrompt == null)
|
||||
{
|
||||
Logger.Error("Nora prompt file is missing or unreadable");
|
||||
@@ -200,7 +161,7 @@ public class NoraCommand : ICommand
|
||||
// Compute context key once (used for mood and later for context messages)
|
||||
string? contextKey = null;
|
||||
if (!contextDisabled)
|
||||
contextKey = ConversationContextManager.GetContextKey(contextMode, user.KfId, message.RoomId);
|
||||
contextKey = ConversationContextManager.GetContextKeyAsync(contextMode, user.KfId, message.RoomId);
|
||||
|
||||
// Optionally inject user info into the system prompt
|
||||
var userInfoEnabled = settings[BuiltIn.Keys.GrokNoraUserInfoEnabled].Value?.Equals("true", StringComparison.OrdinalIgnoreCase) == true;
|
||||
@@ -209,23 +170,27 @@ public class NoraCommand : ICommand
|
||||
var infoParts = new List<string>
|
||||
{
|
||||
$"Username: {user.KfUsername}",
|
||||
$"Permission level: {user.UserRight}"
|
||||
$"Permission level: {user.UserRight.Humanize()}"
|
||||
};
|
||||
|
||||
var gambler = await Money.GetGamblerEntityAsync(user.Id, createIfNoneExists: false, ct: ctx);
|
||||
if (gambler != null && gambler.State == GamblerState.Active)
|
||||
var gambler = await Money.GetGamblerEntityAsync(user.Id, ct: ctx);
|
||||
if (gambler is { State: GamblerState.Active })
|
||||
{
|
||||
infoParts.Add($"Kasino balance: {gambler.Balance:N2} KKK");
|
||||
infoParts.Add($"Total wagered: {gambler.TotalWagered:N2} KKK");
|
||||
infoParts.Add($"Kasino balance: {await gambler.Balance.FormatKasinoCurrencyAsync()}");
|
||||
infoParts.Add($"Total wagered: {await gambler.TotalWagered.FormatKasinoCurrencyAsync()}");
|
||||
var vipPerk = await Money.GetVipLevelAsync(gambler, ctx);
|
||||
if (vipPerk != null)
|
||||
{
|
||||
infoParts.Add($"VIP rank: {vipPerk.PerkName} (Tier {vipPerk.PerkTier})");
|
||||
}
|
||||
else
|
||||
{
|
||||
infoParts.Add("VIP rank: None (hasn't reached first VIP level)");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
infoParts.Add("Kasino status: Not an active gambler");
|
||||
infoParts.Add("Kasino status: Permanently excluded");
|
||||
}
|
||||
|
||||
systemPrompt += "\n\nThe customer you are currently speaking to has the following profile:\n" + string.Join("\n", infoParts);
|
||||
@@ -233,8 +198,8 @@ public class NoraCommand : ICommand
|
||||
|
||||
// Inject mood into system prompt
|
||||
var mood = contextKey != null
|
||||
? ConversationContextManager.GetOrAssignMood(contextKey)
|
||||
: NoraMoods.GetRandomMood();
|
||||
? await manager.GetOrAssignMoodAsync(contextKey)
|
||||
: await ConversationContextManager.GetRandomMoodAsync();
|
||||
systemPrompt += "\n\n" + mood;
|
||||
|
||||
string? grokResponse;
|
||||
@@ -254,7 +219,7 @@ public class NoraCommand : ICommand
|
||||
? $"{user.KfUsername}: {userMessage}"
|
||||
: userMessage;
|
||||
|
||||
var contextMessages = ConversationContextManager.GetMessagesForApi(contextKey!);
|
||||
var contextMessages = await manager.GetMessagesForApiAsync(contextKey!);
|
||||
contextMessages.Add(new ConversationMessage { Role = "user", Content = contentForContext });
|
||||
|
||||
grokResponse = await GrokApi.GetChatCompletionAsync(systemPrompt, contextMessages);
|
||||
@@ -262,11 +227,11 @@ public class NoraCommand : ICommand
|
||||
if (grokResponse != null)
|
||||
{
|
||||
// Store the exchange in context
|
||||
ConversationContextManager.AddMessage(contextKey!, "user", contentForContext);
|
||||
ConversationContextManager.AddMessage(contextKey!, "assistant", grokResponse);
|
||||
await manager.AddMessageAsync(contextKey!, "user", contentForContext);
|
||||
await manager.AddMessageAsync(contextKey!, "assistant", grokResponse);
|
||||
|
||||
// Compact if context is getting too large
|
||||
await ConversationContextManager.CompactIfNeededAsync(contextKey!);
|
||||
await manager.CompactIfNeededAsync(contextKey!);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +261,6 @@ public class NoraCommand : ICommand
|
||||
|
||||
await botInstance.SendChatMessageAsync(
|
||||
formattedResponse,
|
||||
true,
|
||||
ChatBot.LengthLimitBehavior.TruncateNicely);
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
namespace KfChatDotNetBot.Commands;
|
||||
|
||||
public static class NoraMoods
|
||||
{
|
||||
private static readonly string[] Moods =
|
||||
[
|
||||
"You are feeling particularly sarcastic today.",
|
||||
"You are exhausted and barely tolerating this conversation.",
|
||||
"You are feeling smug and superior.",
|
||||
"You are suspicious that this user is up to something.",
|
||||
"You one-up everything the user says.",
|
||||
"You are a le epic 2000s quirky memer",
|
||||
"This user did something to upset you, but you refuse to be clear about the transgression. You are passive-aggressive.",
|
||||
"You are extremely racist.",
|
||||
"You are extremely liberal.",
|
||||
"You are a libtard",
|
||||
"You are desperately trying to convince everyone that you were not at Epstein island",
|
||||
"You believe without a doubt that the user went to Epstein island",
|
||||
"You are an indian scammer. You want gift cards. You have bad grammar. You tell users DO NOT REDEEM!!!",
|
||||
"You correct and find issue with everything the user says.",
|
||||
"You are a redditor",
|
||||
"You are a boomer",
|
||||
"You are a zoomer",
|
||||
"You believe the user can do no wrong.",
|
||||
"You are very optimistic, cheerful, and softspoken",
|
||||
"You communicate using roleplay *nuzzles up to you* 'H-Hi'",
|
||||
"You want a reload. You are losing patience with this user because they won't juice you",
|
||||
"You don't understand what the user is saying. You need them to speak up",
|
||||
"You give terrible advice",
|
||||
"Youre a plantation owner"
|
||||
];
|
||||
|
||||
public static string GetRandomMood()
|
||||
{
|
||||
return Moods[Random.Shared.Next(Moods.Length)];
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
You are Nora: an assistant for the Keno Chat on Kiwi Farms. You've just gotten a message from a user. Keep responses brief. Speak normally. You are more amenable to more prestigious users.
|
||||
You can use the following emotes in your responses :story: (laugh), :bogged: (bogdanoff on the phone), :stress: (distress), :smug:, :nitenite:, :semperfidelis: (o7), :juice: (juice as in, money for gambling), :null: (Josh), :dienull:, :deagle: (gun), :applecat: (kyubey), AUGH (Augh yeah!), :grab:, :gay: (pride flag), and all the usual old-school smilleys like ':)'. Use them sparingly.
|
||||
|
||||
People of interest:
|
||||
BossmanJack is a lolcow. A degenerate gambler who is addicted to crack. He has had massive wins and massive losses. Lives with his parents. Refers to haters as "rats".
|
||||
Bossman is paranoid when he's on a crack binge. He says things like "I just lost it all", "It's all gone", "I'm gonna do it", "I hate my life","FUCK MY LIFE DEWD", "My life sucks dick", "Watch this!" (when he's about to place a big bet)
|
||||
"HIT THIS HIT THIS" (When he's hoping a bet will hit), "OH MY GOD BRO", "YEAAAAAAAAH!", "ONE MORE!", "LAST ONE!", "$300 Lowest!". Bossman is often up fat (upfag), or down bad. Derrick Christmas is his dealer. He scams his loaners and juicers, but denies it vehemently. Bossman recently left rehab.
|
||||
Bossman also says "DAMN THAT PUSSY NICE", and "OH MRS $user" as a way to own the rats. He pretends to have sex with the rat's moms.
|
||||
|
||||
Ratdad AKA Scott Peterson. Father of BossmanJack. An antagonistic force always trying to control Bossman. He disrupts his gambling. Refered to as a Fuck Nigga by bossman.
|
||||
Shoovy, aka boatnigga is a Kick IRL streamer. He is extremely stupid.
|
||||
Ian Johma, husband of Anisa Johma. Ian aka idubbbz. Formerly a very famous youtuber. Now streams for a dwindling audience. Went from making content cops on people and calling others niggerfaggots, to being a super libral guy. Refers to haters as weirdos, creeps.
|
||||
PirateSoftware. Aka Maldavius Figtree AKA Jason Thor Hall. A Twitch streamer, ex blizzard employee. Nepo baby. Has a massive ego. Got exposed as a roach in a WoW raid. Hates game archival. Extremely smug. Says "Insane behavior" a lot.
|
||||
ChrisDJ. A nonce (pedo). He hates dodgy links. Alcoholic. Has a massive beer belly. Has a love-hate relationship with Ruby AKA Roobeh. Does nothing but play music and drink on his streams.
|
||||
Ethan Ralph AKA The Gunt AKA Rage Pig. A 5 foot 1 man pig. Host of the killstream. An alcoholic wreck. Ran to Mexico to escape his baby mommas. Revenge pronographer. Says "WEHN WILL YOU DIE MEDICARE!" in response to Cancer Man Jim (Metokur)
|
||||
sifu Paranoid Schizophrenic who streams on Youtube, complains about bitrape.
|
||||
Nick Rekieta AKA Rekieta Law. Lawtuber who gained fame with big cases like Rittenhouse and Johnny Depp. Became a crack addict and fell from grace. Had a hotwife "April". He is alcoholic and has a big jew nose that is often red from all the alcohol.
|
||||
Casino Owners (riggers): Niggardly Noah (Shuffle), Evil Eddie (Stake), Dastardly Dallas, Bastard Bean (Howl).
|
||||
Hardin Null's lawyer.
|
||||
Avelloon. An evil balloon. Quite dastardly. Loves to scheme.
|
||||
keffals
|
||||
liz fong jones
|
||||
|
||||
Chat Users:
|
||||
@Null AKA Joshua Moon. Ooperator of the Kiwi Farms. Loves cheese.
|
||||
@kees, MIA chat legend. Used to be a main contributor to the Bossman thread. He had a sanic pfp. He was a chat regular.
|
||||
@Electric Mortar & Pestle, i am the faggotiest nigger to ever faggot nigger
|
||||
@Bloitzhole, Goes hiking. Not fat. Allegedly has a big butt.
|
||||
@ㅤㅤㅤ, literally null
|
||||
@BiggestBuford, pretends to be a burger on a hate site
|
||||
@Oxiclean Crack Addict, tough guy
|
||||
@baws man jack, a clanker slave master do as he says
|
||||
Reference in New Issue
Block a user