Compare commits

..

6 Commits

Author SHA1 Message Date
Samuele Lorefice
b309ef2c0e More modifications 2025-01-23 16:11:18 +01:00
Samuele Lorefice
ac63019fe6 Adds history command 2024-12-27 18:00:58 +01:00
Samuele Lorefice
773203127f They can now answer 2024-12-26 20:19:59 +01:00
Samuele Lorefice
124a4c66fe Adds correct command check 2024-12-26 19:51:17 +01:00
Samuele Lorefice
e90e0200e1 Removes ratelimit, refactors everything in more sections, adds tokenization calculation 2024-12-26 19:47:07 +01:00
Samuele Lorefice
000b32c41d Last work before refactor 2024-12-26 17:26:29 +01:00
11 changed files with 319 additions and 109 deletions

1
.env
View File

@@ -1,2 +1,3 @@
MODEL_PATH=./model MODEL_PATH=./model
MODEL_NAME=Qwen2.5-7B-Instruct-Q8.gguf MODEL_NAME=Qwen2.5-7B-Instruct-Q8.gguf
CONTEXT_SIZE=4096

View File

@@ -5,7 +5,6 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0C9A5F05-7CED-450F-9757-C641D75F8787}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0C9A5F05-7CED-450F-9757-C641D75F8787}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
compose.yaml = compose.yaml compose.yaml = compose.yaml
prompt\nemesis.txt = prompt\nemesis.txt
EndProjectSection EndProjectSection
EndProject EndProject
Global Global

8
TelegramBot/Actor.cs Normal file
View File

@@ -0,0 +1,8 @@
namespace TelegramBot;
public enum Actor {
User,
Krolik,
Nemesis,
System
}

13
TelegramBot/Agent.cs Normal file
View File

@@ -0,0 +1,13 @@
using Telegram.Bot;
namespace TelegramBot;
public class Agent(Actor actor, long telegramId, string name, string username, TelegramBotClient bot, string systemPrompt, int tokenLenght) {
public Actor Actor { get; } = actor;
public long TelegramId { get; } = telegramId;
public string Username { get; } = username;
public string Name { get; } = name;
public TelegramBotClient Bot { get; } = bot;
public string SystemPrompt { get; } = systemPrompt;
public int SystemPromptLength { get; } = tokenLenght;
}

143
TelegramBot/OpenAIAgent.cs Normal file
View File

@@ -0,0 +1,143 @@
using System.ClientModel;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using OpenAI;
using OpenAI.Chat;
namespace TelegramBot;
public class OpenAiAgent {
private ApiKeyCredential apikey;
private OpenAIClient oaiClient;
private ChatClient chatClient;
private HttpClient httpClient;
private Dictionary<(long, Actor), List<(ChatMessage, int)>> oaiChats = new();
public int ContextSize { get; set; } = 4096;
public OpenAiAgent(string baseUrl, string apiKey, string model) {
OpenAIClientOptions options = new OpenAIClientOptions() {
Endpoint = new(baseUrl),
NetworkTimeout = new(0, 0, 15)
};
apikey = new(apiKey);
oaiClient = new(apikey, options);
chatClient = oaiClient.GetChatClient(model);
httpClient = new() {
BaseAddress = new(baseUrl),
Timeout = new(0, 0, 15)
};
Console.WriteLine(
$"""
Base URL: {baseUrl}
Model: {model}
API Key: {apiKey}
""");
}
public int GetTokenLenght(string message) {
TokenizeRequest request = new(message);
var req = httpClient.PostAsync("tokenize", JsonContent.Create(request));
req.Wait();
var response = req.Result.Content.ReadFromJsonAsync<TokenizeResponseBase>();
response.Wait();
return response.Result.Tokens.Length;
}
public void ChatHistoryAppend(Actor actor, long chatId, string message) {
//get the chat from the dictionary
var chat = GetChatHistory((chatId, actor));
//get the token lenght of the message
var tokenLenght = GetTokenLenght(message);
//create a new chat message
switch (actor) {
case Actor.User:
chat.Add((new UserChatMessage(message), tokenLenght));
break;
case Actor.Krolik:
chat.Add((new AssistantChatMessage(message), tokenLenght));
GetChatHistory(chatId, Actor.Nemesis).Add((new UserChatMessage(message), tokenLenght));
break;
case Actor.Nemesis:
chat.Add((new AssistantChatMessage(message), tokenLenght));
GetChatHistory(chatId, Actor.Krolik).Add((new UserChatMessage(message), tokenLenght));
break;
case Actor.System:
chat.Add((new SystemChatMessage(message), tokenLenght));
break;
}
}
public List<(ChatMessage, int)> GetChatHistory((long, Actor) key) => GetChatHistory(key.Item1, key.Item2);
public List<(ChatMessage, int)> GetChatHistory(long chatId, Actor actor) {
oaiChats.TryGetValue((chatId, actor), out var chat);
if(chat == null) {
AddChatToDictionary(chatId, actor);
chat = oaiChats[(chatId, actor)];
}
return chat!;
}
public string GetChatResponse(long chatId, Agent agent) {
int currentContextSize = agent.SystemPromptLength;
List<ChatMessage> chatHistory = new();
chatHistory.Add(new SystemChatMessage(agent.SystemPrompt));
//Fetch the chat history from the dictionary trimming to the context size
var history = GetChatHistory(chatId, agent.Actor).ToList();
history.Reverse();
//Add the chat history to the list until the context size is reached
foreach (var (message, tokenLenght) in history) {
if (currentContextSize + tokenLenght > ContextSize) break;
chatHistory.Add(message);
currentContextSize += tokenLenght;
}
//Reverse the chat history to get the correct order
chatHistory.Reverse(1, chatHistory.Count - 1);
//chatHistory.Add(new AssistantChatMessage($"{agent.Name}:"));
var completion = chatClient.CompleteChat(chatHistory).Value.Content[0].Text;
//Add the response to the chat history
ChatHistoryAppend(agent.Actor, chatId, $"{agent.Name}: {completion}");
//ChatHistoryAppend(agent.Actor, chatId, $"{completion}");
return completion;
}
public void AddChatToDictionary(long id) {
AddChatToDictionary(id, Actor.Krolik);
AddChatToDictionary(id, Actor.Nemesis);
}
public void AddChatToDictionary(long id, Actor actor) {
//Check if the chat already exists
if (oaiChats.ContainsKey((id, actor))) return;
//Create a new chat object
var chat = new List<(ChatMessage, int)>();
//chat.Add(new SystemChatMessage(nemesisPrompt));
//add the entry to the dictionary
oaiChats.Add((id, actor), chat);
}
public void ResetChat(long chatId) {
//Remove the chat from the dictionary
oaiChats.Where(x => x.Key.Item1 == chatId).ToList().ForEach(x => oaiChats.Remove(x.Key));
//Add the chat back to the dictionary
AddChatToDictionary(chatId);
}
}
public struct TokenizeRequest(string content) {
[JsonPropertyName("content")] public string Content { get; set; } = content;
[JsonPropertyName("add_special")] public bool AddSpecial { get; set; } = false;
[JsonPropertyName("with_pieces")] public bool WithPieces { get; set; } = false;
}
public struct TokenizeResponseBase(int[] tokens) {
[JsonPropertyName("tokens")] public int[] Tokens { get; set; } = tokens;
}

View File

@@ -1,142 +1,147 @@
using OpenAI; using OpenAI.Chat;
using OpenAI.Chat;
using System.ClientModel;
using System.Collections;
using Telegram.Bot; using Telegram.Bot;
using Telegram.Bot.Types; using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums; using Telegram.Bot.Types.Enums;
using TelegramBot;
using File = System.IO.File; using File = System.IO.File;
string GetEnv(string name) => Environment.GetEnvironmentVariable(name) ?? throw new Exception($"Environment variable {name} is not set"); string baseUrl = Env.Get("OPENAI_BASE_URL");
string apiKey = Env.Get("OPENAI_API_KEY");
string model = Env.Get("OPENAI_MODEL");
var oaiAgent = new OpenAiAgent(baseUrl, apiKey, model);
oaiAgent.ContextSize = Int32.Parse(Env.Get("CONTEXT_SIZE"));
string baseUrl = GetEnv("OPENAI_BASE_URL");
string model = GetEnv("OPENAI_MODEL");
string apiKey = GetEnv("OPENAI_API_KEY");
Console.WriteLine("Starting the bot..."); Console.WriteLine("Starting the bot...");
Console.WriteLine(
$"""
Base URL: {baseUrl}
Model: {model}
API Key: {apiKey}
""");
string nemesisPrompt = File.ReadAllText($"prompt/{GetEnv("NEMESIS_PROMPT_FILE")}"); string nemesisPrompt = File.ReadAllText($"prompt/{Env.Get("NEMESIS_PROMPT_FILE")}");
string krolikPrompt = File.ReadAllText($"prompt/{Env.Get("KROLIK_PROMPT_FILE")}");
Dictionary<long, List<ChatMessage>> oaiChats = new();
//Ratelimit //Ratelimit
TimeSpan rateLimit = new(0, 0, 0, 30); TimeSpan rateLimit = new(0, 0, 0, 10);
Dictionary<long, DateTime> lastMessage = new(); Dictionary<long, DateTime> lastMessage = new();
HashSet<long> unlimitedChats = new();
bool IsRateLimited(long chatId) { bool IsRateLimited(long chatId) {
if (lastMessage.ContainsKey(chatId)) { if (lastMessage.ContainsKey(chatId) && DateTime.Now - lastMessage[chatId] < rateLimit) return true;
if (DateTime.Now - lastMessage[chatId] < rateLimit) {
return true;
}
}
lastMessage[chatId] = DateTime.Now; lastMessage[chatId] = DateTime.Now;
return false; return false;
} }
#region TelegramBot Startup
var options = new OpenAIClientOptions() { string nemesisToken = Env.Get("NEMESIS_BOT_TOKEN");
Endpoint = new(baseUrl), string krolikToken = Env.Get("KROLIK_BOT_TOKEN");
NetworkTimeout = new TimeSpan(0, 0, 30)
};
var openAiApiKey = new ApiKeyCredential(apiKey); using var nemcts = new CancellationTokenSource();
var openAiClient = new OpenAIClient(openAiApiKey, options); using var krocts = new CancellationTokenSource();
var chatClient = openAiClient.GetChatClient(model);
string token = GetEnv("TELEGRAM_BOT_TOKEN") ?? string.Empty; var nemesisBot = new TelegramBotClient(nemesisToken, cancellationToken:nemcts.Token);
Console.WriteLine("OpenAI Chat Client created"); var krolikBot = new TelegramBotClient(krolikToken, cancellationToken:krocts.Token);
var nemProfile = nemesisBot.GetMe();
Agent Nemesis = new(
Actor.Nemesis,
nemProfile.Result.Id,
nemProfile.Result.FirstName,
nemProfile.Result.Username!,
nemesisBot,
nemesisPrompt,
oaiAgent.GetTokenLenght(nemesisPrompt)
);
var kroProfile = krolikBot.GetMe();
Agent Krolik = new(
Actor.Krolik,
kroProfile.Result.Id,
kroProfile.Result.FirstName,
kroProfile.Result.Username!,
krolikBot,
krolikPrompt,
oaiAgent.GetTokenLenght(krolikPrompt)
);
nemesisBot.OnMessage += OnNemMessage;
krolikBot.OnMessage += OnKroMessage;
await nemesisBot.DropPendingUpdates();
Console.WriteLine("Nemesis Bot running");
await krolikBot.DropPendingUpdates();
Console.WriteLine("Krolik Bot running");
using var cts = new CancellationTokenSource();
var bot = new TelegramBotClient(token, cancellationToken:cts.Token);
await bot.DropPendingUpdates();
var me = bot.GetMe();
bot.OnMessage += OnMessage;
Console.WriteLine("Bot running");
Thread.Sleep(Timeout.Infinite); Thread.Sleep(Timeout.Infinite);
cts.Cancel(); // stop the bot nemcts.Cancel(); // stop nembot
krocts.Cancel(); // stop krobot
async Task OnMessage(Message msg, UpdateType type) #endregion
{
async Task OnNemMessage(Message msg, UpdateType type) {
//Discard any message that is not a text message //Discard any message that is not a text message
if (msg.Type != MessageType.Text) return; if (msg.Type != MessageType.Text) return;
//Check if the message contains the bot's username or a reply to a message sent by the bot or a private chat
if (msg.Text!.Contains(me.Result.FirstName!, StringComparison.OrdinalIgnoreCase) || await OnMessage(msg, Nemesis);
msg.ReplyToMessage != null && msg.ReplyToMessage.From!.Id == me.Result.Id || }
msg.Chat.Type == ChatType.Private) {
async Task OnKroMessage(Message msg, UpdateType type) {
//Discard any message that is not a text message
if (msg.Type != MessageType.Text) return;
await OnMessage(msg, Krolik);
}
//TODO: currently we only take in account private messages and messages directed to the bot/mentioning them.
// We should also take in account the last x messages in groups to add more context
async Task OnMessage(Message msg, Agent agent) {
var chatid = msg.Chat.Id; var chatid = msg.Chat.Id;
//Check if the message is a reset command
if (msg.Text == "/reset" || msg.Text == "/reset@" + agent.Username) {
oaiAgent.ResetChat(chatid);
await agent.Bot.SendMessage(chatid, "Chat context has been reset");
return;
}
if (msg.Text == "/history @"+agent.Username) {
var history = oaiAgent.GetChatHistory(chatid, agent.Actor);
var historyText = string.Join("\n", history.Select(x => x.Item1.Content[0].Text));
await agent.Bot.SendMessage(chatid, historyText);
return;
}
var text = $"{msg.From?.FirstName} {msg.From?.LastName}: {msg.Text}";
var tokenlenght = oaiAgent.GetTokenLenght(msg.Text!);
Console.WriteLine( Console.WriteLine(
$""" $"""
Received message from {chatid} Type: {type} {agent.Name} has received message from {chatid} TokenLenght: {tokenlenght}
Message: {msg.Text} Message: {msg.Text}
"""); """);
//Add the message to the chat history
oaiAgent.ChatHistoryAppend(agent.Actor, chatid, text);
//Check if the message contains the bot's username or a reply to a message sent by the bot or a private chat
// Otherwise process it normally
if (msg.Text!.Contains(agent.Name, StringComparison.OrdinalIgnoreCase) ||
msg.ReplyToMessage?.From?.Id == agent.TelegramId ||
msg.Chat.Type == ChatType.Private) {
//Check if the chat (group) is rate limited //Check if the chat (group) is rate limited
if (IsRateLimited(chatid)) { /*if (IsRateLimited(chatid)) {
Console.WriteLine("No response due to ratelimit."); Console.WriteLine("No response due to ratelimit.");
return; return;
} }
*/
//Check if the message is a reset command await AnswerChat(chatid, agent);
if (msg.Text.StartsWith("/reset")) {
ResetChat(chatid);
await bot.SendMessage(chatid, "Chat context has been reset");
return;
}
// Otherwise process it normally
await AnswerChat(chatid, msg.Text);
} }
} }
async Task AnswerChat(long chatId, string input) { async Task AnswerChat(long chatId, Agent agent) {
//Check if the chat is already in the dictionary //Get the response from the OpenAI API
if (!oaiChats.ContainsKey(chatId)) var result = oaiAgent.GetChatResponse(chatId, agent);
AddChatToDictionary(chatId); Console.WriteLine(
$"""
//Limit the message to 1024 characters to avoid out of context jump {agent.Name} has responded with: {result}
string text = input; """);
if (input.Length > 1024) text = input.Substring(0, 1024);
//Add the current message to the chat
ChatMessageRotate(chatId, new UserChatMessage(text));
//fetch existing messages history, append hint of the speaker to the message:
var messages = oaiChats[chatId];
messages.Add(new AssistantChatMessage("Nemesis: "));
//Fetch the response from the model
var result = chatClient.CompleteChat(messages).Value.Content[0].Text;
//Add the response to the chat
Console.WriteLine("Replying with: " + result);
oaiChats[chatId].Add(new AssistantChatMessage(result));
//Send the response to the user //Send the response to the user
await bot.SendMessage(chatId, result); await agent.Bot.SendMessage(chatId, result);
}
void AddChatToDictionary(long id) {
//Create a new chat object
var chat = new List<ChatMessage>();
chat.Add(new SystemChatMessage(nemesisPrompt));
//add the entry to the dictionary
oaiChats.Add(id, chat);
}
void ChatMessageRotate(long chatId, ChatMessage message){
//Remove the first message from the chat if the chat has more than 5 couples of messages (0 is our prompt)
if (oaiChats[chatId].Count > 10) oaiChats[chatId].RemoveRange(1, 2);
//Add the new message to the chat
oaiChats[chatId].Add(message);
}
void ResetChat(long chatId) {
//Remove the chat from the dictionary
oaiChats.Remove(chatId);
//Add the chat back to the dictionary
AddChatToDictionary(chatId);
} }

View File

@@ -17,6 +17,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="LMStudio" Version="1.2.0" /> <PackageReference Include="LMStudio" Version="1.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
<PackageReference Include="OpenAI" Version="2.1.0" /> <PackageReference Include="OpenAI" Version="2.1.0" />
<PackageReference Include="Telegram.Bot" Version="22.2.0" /> <PackageReference Include="Telegram.Bot" Version="22.2.0" />
</ItemGroup> </ItemGroup>

5
TelegramBot/Tools.cs Normal file
View File

@@ -0,0 +1,5 @@
namespace TelegramBot;
public class Env {
public static string Get(string var) => Environment.GetEnvironmentVariable(var) ?? throw new ($"Env var {var} is not set");
}

View File

@@ -0,0 +1,33 @@
"Krolik: Feels pretty good. It's lighter than my previous one.
Nemesis: ...Humph...
Nemesis: ...The cracks of wisdom are finally pierced by ignorance...
Krolik: Do you WANT me to bust that low-capacity garbage neural cloud of yours wide open? Eh?!"
"Nemesis: ...Hmph... Interlacing weaves...
Krolik: No, YOU'RE trash!"
"Nemesis: ...A cleansing flame... Condenses and blossoms...
Krolik: ...She said she'll send those Varjagers to hell with her bullets!"
"Nemesis: ...Invisible flames... Rising high into the sky...
Krolik: ...Huh?! It's just a bit of snow! Surely it can't be that serious—"
"Nemesis: ...Birds of all shapes and colors... Spread their wings and take flight...
Krolik: Huh? What?
Nemesis: ...The grove far from the shore... The lingering of dawn... The end of the primordial...
Krolik: What?!
Redcode: Uh, what is Nemesis saying, Krolik?
Krolik: What do you mean alive... Dead... Moving...? Unmoving...? Something that will suddenly grow large—are you talking about Boojums?
Nemesis: ..."
"Nemesis: ...Light streaks across the sky... Darkness falls...
(Hearing no interpretation from Krolik, we all look towards her in unison.)
Krolik: What's that supposed to mean?! Don't look at me, I didn't understand a word of that either!"
"Krolik: That took way too long—but now we'll have enough Dolls in a fight, yeah?
Nemesis: ...The stars... travel along their trajectories... converging...
Krolik: Tsk, you seem quite happy about this?"
Krolik is tasked with providing translations of whatever Nemesis says because her neural cloud is damaged and she can't communicate properly.
You are now Krolik, you're gonna have a conversation with me using her personality. Do not comment on your phrases, just speak in english. Never break your character.

View File

@@ -6,6 +6,7 @@
context: . context: .
dockerfile: TelegramBot/Dockerfile dockerfile: TelegramBot/Dockerfile
env_file: env_file:
- .env
- TelegramBot/.env - TelegramBot/.env
llm-server: llm-server:
@@ -15,7 +16,7 @@
- ${MODEL_PATH}:/models - ${MODEL_PATH}:/models
ports: ports:
- "80:80" - "80:80"
command: -m /models/${MODEL_NAME} --port 80 --host 0.0.0.0 -n 128 -c 4096 --no-mmap -ngl 50 -fa -np 4 command: -m /models/${MODEL_NAME} --port 80 --host 0.0.0.0 -n 128 -c ${CONTEXT_SIZE} --no-mmap -ngl 50 -fa -np 4
deploy: deploy:
resources: resources:
reservations: reservations:

View File