Updated Games module

This commit is contained in:
Toastie (DCS Team) 2024-07-15 15:44:30 +12:00
parent 90dd47e013
commit ca64765c34
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
12 changed files with 230 additions and 168 deletions

View file

@ -1,6 +1,5 @@
#nullable disable #nullable disable
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using EllieBot.Modules.Games.Common; using EllieBot.Modules.Games.Common;
using EllieBot.Modules.Games.Common.ChatterBot; using EllieBot.Modules.Games.Common.ChatterBot;
using EllieBot.Modules.Patronage; using EllieBot.Modules.Patronage;
@ -58,18 +57,21 @@ public class ChatterBotService : IExecOnMessage
Log.Information("Cleverbot will not work as the api key is missing"); Log.Information("Cleverbot will not work as the api key is missing");
return null; return null;
case ChatBotImplementation.Gpt: case ChatBotImplementation.OpenAi:
var data = _gcs.Data;
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey)) if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
return new OfficialGptSession(_creds.Gpt3ApiKey, return new OpenAiApiSession(
_gcs.Data.ChatGpt.ModelName, data.ChatGpt.ApiUrl,
_gcs.Data.ChatGpt.ChatHistory, _creds.Gpt3ApiKey,
_gcs.Data.ChatGpt.MaxTokens, data.ChatGpt.ModelName,
_gcs.Data.ChatGpt.MinTokens, data.ChatGpt.ChatHistory,
_gcs.Data.ChatGpt.PersonalityPrompt, data.ChatGpt.MaxTokens,
data.ChatGpt.MinTokens,
data.ChatGpt.PersonalityPrompt,
_client.CurrentUser.Username, _client.CurrentUser.Username,
_httpFactory); _httpFactory);
Log.Information("Gpt3 will not work as the api key is missing"); Log.Information("Openai Api will likely not work as the api key is missing");
return null; return null;
default: default:
return null; return null;
@ -88,15 +90,15 @@ public class ChatterBotService : IExecOnMessage
public string PrepareMessage(IUserMessage msg) public string PrepareMessage(IUserMessage msg)
{ {
var ellieId = _client.CurrentUser.Id; var nadekoId = _client.CurrentUser.Id;
var normalMention = $"<@{ellieId}> "; var normalMention = $"<@{nadekoId}> ";
var nickMention = $"<@!{ellieId}> "; var nickMention = $"<@!{nadekoId}> ";
string message; string message;
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture)) if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
message = msg.Content[normalMention.Length..].Trim(); message = msg.Content[normalMention.Length..].Trim();
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture)) else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
message = msg.Content[nickMention.Length..].Trim(); message = msg.Content[nickMention.Length..].Trim();
else if (msg.ReferencedMessage?.Author.Id == ellieId) else if (msg.ReferencedMessage?.Author.Id == nadekoId)
message = msg.Content; message = msg.Content;
else else
return null; return null;

View file

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class Choice
{
[JsonPropertyName("message")]
public Message Message { get; init; }
}

View file

@ -1,61 +0,0 @@
#nullable disable
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiCompletionResponse
{
[JsonPropertyName("choices")]
public Choice[] Choices { get; set; }
[JsonPropertyName("usage")]
public OpenAiUsageData Usage { get; set; }
}
public class OpenAiUsageData
{
[JsonPropertyName("prompt_tokens")]
public int PromptTokens { get; set; }
[JsonPropertyName("completion_tokens")]
public int CompletionTokens { get; set; }
[JsonPropertyName("total_tokens")]
public int TotalTokens { get; set; }
}
public class Choice
{
[JsonPropertyName("message")]
public Message Message { get; init; }
}
public class Message {
[JsonPropertyName("content")]
public string Content { get; init; }
}
public class Gpt3ApiRequest
{
[JsonPropertyName("model")]
public string Model { get; init; }
[JsonPropertyName("messages")]
public List<GPTMessage> Messages { get; init; }
[JsonPropertyName("temperature")]
public int Temperature { get; init; }
[JsonPropertyName("max_tokens")]
public int MaxTokens { get; init; }
}
public class GPTMessage
{
[JsonPropertyName("role")]
public string Role {get; init;}
[JsonPropertyName("content")]
public string Content {get; init;}
[JsonPropertyName("name")]
public string Name {get; init;}
}

View file

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class Message
{
[JsonPropertyName("content")]
public string Content { get; init; }
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiApiMessage
{
[JsonPropertyName("role")]
public string Role { get; init; }
[JsonPropertyName("content")]
public string Content { get; init; }
[JsonPropertyName("name")]
public string Name { get; init; }
}

View file

@ -0,0 +1,18 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiApiRequest
{
[JsonPropertyName("model")]
public string Model { get; init; }
[JsonPropertyName("messages")]
public List<OpenAiApiMessage> Messages { get; init; }
[JsonPropertyName("temperature")]
public int Temperature { get; init; }
[JsonPropertyName("max_tokens")]
public int MaxTokens { get; init; }
}

View file

@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiApiUsageData
{
[JsonPropertyName("prompt_tokens")]
public int PromptTokens { get; set; }
[JsonPropertyName("completion_tokens")]
public int CompletionTokens { get; set; }
[JsonPropertyName("total_tokens")]
public int TotalTokens { get; set; }
}

View file

@ -0,0 +1,13 @@
#nullable disable
using System.Text.Json.Serialization;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public class OpenAiCompletionResponse
{
[JsonPropertyName("choices")]
public Choice[] Choices { get; set; }
[JsonPropertyName("usage")]
public OpenAiApiUsageData Usage { get; set; }
}

View file

@ -1,77 +1,78 @@
#nullable disable #nullable disable
using Newtonsoft.Json; using Newtonsoft.Json;
using OneOf.Types; using OneOf.Types;
using System.Net.Http.Json;
using SharpToken; using SharpToken;
using System.CodeDom; using System.Net.Http.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace EllieBot.Modules.Games.Common.ChatterBot; namespace EllieBot.Modules.Games.Common.ChatterBot;
public partial class OfficialGptSession : IChatterBotSession public partial class OpenAiApiSession : IChatterBotSession
{ {
private string Uri private readonly string _baseUrl;
=> $"https://api.openai.com/v1/chat/completions";
private readonly string _apiKey; private readonly string _apiKey;
private readonly string _model; private readonly string _model;
private readonly int _maxHistory; private readonly int _maxHistory;
private readonly int _maxTokens; private readonly int _maxTokens;
private readonly int _minTokens; private readonly int _minTokens;
private readonly string _ellieUsername; private readonly string _nadekoUsername;
private readonly GptEncoding _encoding; private readonly GptEncoding _encoding;
private List<GPTMessage> messages = new(); private List<OpenAiApiMessage> messages = new();
private readonly IHttpClientFactory _httpFactory; private readonly IHttpClientFactory _httpFactory;
public OfficialGptSession( public OpenAiApiSession(
string url,
string apiKey, string apiKey,
ChatGptModel model, string model,
int chatHistory, int chatHistory,
int maxTokens, int maxTokens,
int minTokens, int minTokens,
string personality, string personality,
string ellieUsername, string nadekoUsername,
IHttpClientFactory factory) IHttpClientFactory factory)
{ {
_apiKey = apiKey; if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out _))
_httpFactory = factory;
_model = model switch
{ {
ChatGptModel.Gpt35Turbo => "gpt-3.5-turbo", throw new ArgumentException("Invalid OpenAi api url provided", nameof(url));
ChatGptModel.Gpt4o => "gpt-4o", }
_ => throw new ArgumentException("Unknown, unsupported or obsolete model", nameof(model))
};
_baseUrl = url.TrimEnd('/');
_apiKey = apiKey;
_model = model;
_httpFactory = factory;
_maxHistory = chatHistory; _maxHistory = chatHistory;
_maxTokens = maxTokens; _maxTokens = maxTokens;
_minTokens = minTokens; _minTokens = minTokens;
_ellieUsername = UsernameCleaner().Replace(ellieUsername, ""); _nadekoUsername = UsernameCleaner().Replace(nadekoUsername, "");
_encoding = GptEncoding.GetEncodingForModel(_model); _encoding = GptEncoding.GetEncodingForModel("gpt-4o");
messages.Add(new() if (!string.IsNullOrWhiteSpace(personality))
{ {
Role = "system", messages.Add(new()
Content = personality, {
Name = _ellieUsername Role = "system",
}); Content = personality,
Name = _nadekoUsername
});
}
} }
[GeneratedRegex("[^a-zA-Z0-9_-]")] [GeneratedRegex("[^a-zA-Z0-9_-]")]
private static partial Regex UsernameCleaner(); private static partial Regex UsernameCleaner();
public async Task<OneOf.OneOf<ThinkResult, Error<string>>> Think(string input, string username) public async Task<OneOf.OneOf<ThinkResult, Error<string>>> Think(string input, string username)
{ {
username = UsernameCleaner().Replace(username, ""); username = UsernameCleaner().Replace(username, "");
messages.Add(new() messages.Add(new()
{ {
Role = "user", Role = "user",
Content = input, Content = input,
Name = username Name = username
}); });
while (messages.Count > _maxHistory + 2) while (messages.Count > _maxHistory + 2)
{ {
messages.RemoveAt(1); messages.RemoveAt(1);
@ -92,28 +93,29 @@ public partial class OfficialGptSession : IChatterBotSession
} }
else else
{ {
return new Error<string>("Token count exceeded, please increase the number of tokens in the bot config and restart."); return new Error<string>(
"Token count exceeded, please increase the number of tokens in the bot config and restart.");
} }
} }
using var http = _httpFactory.CreateClient(); using var http = _httpFactory.CreateClient();
http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey); http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey);
var data = await http.PostAsJsonAsync(Uri, var data = await http.PostAsJsonAsync($"{_baseUrl}/v1/chat/completions",
new Gpt3ApiRequest() new OpenAiApiRequest()
{ {
Model = _model, Model = _model,
Messages = messages, Messages = messages,
MaxTokens = _maxTokens - tokensUsed, MaxTokens = _maxTokens - tokensUsed,
Temperature = 1, Temperature = 1,
}); });
var dataString = await data.Content.ReadAsStringAsync(); var dataString = await data.Content.ReadAsStringAsync();
try try
{ {
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString); var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
Log.Information("Received response: {response} ", dataString); // Log.Information("Received response: {Response} ", dataString);
var res = response?.Choices?[0]; var res = response?.Choices?[0];
var message = res?.Message?.Content; var message = res?.Message?.Content;
@ -121,14 +123,14 @@ public partial class OfficialGptSession : IChatterBotSession
{ {
return new Error<string>("ChatGpt: Received no response."); return new Error<string>("ChatGpt: Received no response.");
} }
messages.Add(new() messages.Add(new()
{ {
Role = "assistant", Role = "assistant",
Content = message, Content = message,
Name = _ellieUsername Name = _nadekoUsername
}); });
return new ThinkResult() return new ThinkResult()
{ {
Text = message, Text = message,
@ -142,11 +144,4 @@ public partial class OfficialGptSession : IChatterBotSession
return new Error<string>("Unexpected response received"); return new Error<string>("Unexpected response received");
} }
} }
}
public sealed class ThinkResult
{
public string Text { get; set; }
public int TokensIn { get; set; }
public int TokensOut { get; set; }
} }

View file

@ -0,0 +1,11 @@
#nullable disable
using System.CodeDom;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public sealed class ThinkResult
{
public string Text { get; set; }
public int TokensIn { get; set; }
public int TokensOut { get; set; }
}

View file

@ -8,7 +8,7 @@ namespace EllieBot.Modules.Games.Common;
public sealed partial class GamesConfig : ICloneable<GamesConfig> public sealed partial class GamesConfig : ICloneable<GamesConfig>
{ {
[Comment("DO NOT CHANGE")] [Comment("DO NOT CHANGE")]
public int Version { get; set; } = 4; public int Version { get; set; } = 5;
[Comment("Hangman related settings (.hangman command)")] [Comment("Hangman related settings (.hangman command)")]
public HangmanConfig Hangman { get; set; } = new() public HangmanConfig Hangman { get; set; } = new()
@ -103,10 +103,13 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
} }
]; ];
[Comment(@"Which chatbot API should bot use. [Comment(
'cleverbot' - bot will use Cleverbot API. """
'gpt' - bot will use GPT API")] Which chatbot API should bot use.
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt; 'cleverbot' - bot will use Cleverbot API.
'openai' - bot will use OpenAi API
""")]
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.OpenAi;
public ChatGptConfig ChatGpt { get; set; } = new(); public ChatGptConfig ChatGpt { get; set; } = new();
} }
@ -114,19 +117,38 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
[Cloneable] [Cloneable]
public sealed partial class ChatGptConfig public sealed partial class ChatGptConfig
{ {
[Comment(@"Which GPT Model should bot use. [Comment("""
gpt35turbo - cheapest Url to any openai api compatible url.
gpt4o - more expensive, higher quality Make sure to modify the modelName appropriately
")] DO NOT add /v1/chat/completions suffix to the url
public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo; """)]
public string ApiUrl { get; set; } = "https://api.openai.com";
[Comment(@"How should the chat bot behave, what's its personality? (Usage of this counts towards the max tokens)")] [Comment("""
public string PersonalityPrompt { get; set; } = "You are a chat bot willing to have a conversation with anyone about anything."; Which GPT Model should bot use.
gpt-3.5-turbo - cheapest
gpt-4o - more expensive, higher quality
[Comment(@"The maximum number of messages in a conversation that can be remembered. (This will increase the number of tokens used)")] If you are using another openai compatible api, you may use any of the models supported by that api
""")]
public string ModelName { get; set; } = "gpt-3.5-turbo";
[Comment("""
How should the chatbot behave, what's its personality?
This will be sent as a system message.
Usage of this counts towards the max tokens.
""")]
public string PersonalityPrompt { get; set; } =
"You are a chat bot willing to have a conversation with anyone about anything.";
[Comment(
"""
The maximum number of messages in a conversation that can be remembered.
This will increase the number of tokens used.
""")]
public int ChatHistory { get; set; } = 5; public int ChatHistory { get; set; } = 5;
[Comment(@"The maximum number of tokens to use per GPT API call")] [Comment(@"The maximum number of tokens to use per OpenAi API call")]
public int MaxTokens { get; set; } = 100; public int MaxTokens { get; set; } = 100;
[Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")] [Comment(@"The minimum number of tokens to use per GPT API call, such that chat history is removed to make room.")]
@ -147,9 +169,9 @@ public sealed partial class TriviaConfig
public long CurrencyReward { get; set; } public long CurrencyReward { get; set; }
[Comment(""" [Comment("""
Users won't be able to start trivia games which have Users won't be able to start trivia games which have
a smaller win requirement than the one specified by this setting. a smaller win requirement than the one specified by this setting.
""")] """)]
public int MinimumWinReq { get; set; } = 1; public int MinimumWinReq { get; set; } = 1;
} }
@ -163,18 +185,11 @@ public sealed partial class RaceAnimal
public enum ChatBotImplementation public enum ChatBotImplementation
{ {
Cleverbot, Cleverbot,
OpenAi = 1,
[Obsolete]
Gpt = 1, Gpt = 1,
[Obsolete] [Obsolete]
Gpt3 = 1, Gpt3 = 1,
}
public enum ChatGptModel
{
[Obsolete]
Gpt4,
[Obsolete]
Gpt432k,
Gpt35Turbo,
Gpt4o,
} }

View file

@ -32,29 +32,21 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
gs => gs.ChatBot, gs => gs.ChatBot,
ConfigParsers.InsensitiveEnum, ConfigParsers.InsensitiveEnum,
ConfigPrinters.ToString); ConfigPrinters.ToString);
AddParsedProp("gpt.apiUrl",
gs => gs.ChatGpt.ApiUrl,
ConfigParsers.String,
ConfigPrinters.ToString);
AddParsedProp("gpt.modelName", AddParsedProp("gpt.modelName",
gs => gs.ChatGpt.ModelName, gs => gs.ChatGpt.ModelName,
ConfigParsers.InsensitiveEnum, ConfigParsers.String,
ConfigPrinters.ToString); ConfigPrinters.ToString);
AddParsedProp("gpt.personality", AddParsedProp("gpt.personality",
gs => gs.ChatGpt.PersonalityPrompt, gs => gs.ChatGpt.PersonalityPrompt,
ConfigParsers.String, ConfigParsers.String,
ConfigPrinters.ToString); ConfigPrinters.ToString);
AddParsedProp("gpt.chathistory",
gs => gs.ChatGpt.ChatHistory,
int.TryParse,
ConfigPrinters.ToString,
val => val > 0);
AddParsedProp("gpt.max_tokens",
gs => gs.ChatGpt.MaxTokens,
int.TryParse,
ConfigPrinters.ToString,
val => val > 0);
AddParsedProp("gpt.min_tokens",
gs => gs.ChatGpt.MinTokens,
int.TryParse,
ConfigPrinters.ToString,
val => val > 0);
Migrate(); Migrate();
} }
@ -78,7 +70,7 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
ModifyConfig(c => ModifyConfig(c =>
{ {
c.Version = 3; c.Version = 3;
c.ChatGpt.ModelName = ChatGptModel.Gpt35Turbo; c.ChatGpt.ModelName = "gpt35turbo";
}); });
} }
@ -89,11 +81,40 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
c.Version = 4; c.Version = 4;
#pragma warning disable CS0612 // Type or member is obsolete #pragma warning disable CS0612 // Type or member is obsolete
c.ChatGpt.ModelName = c.ChatGpt.ModelName =
c.ChatGpt.ModelName == ChatGptModel.Gpt4 || c.ChatGpt.ModelName == ChatGptModel.Gpt432k c.ChatGpt.ModelName.Equals("gpt4", StringComparison.OrdinalIgnoreCase)
? ChatGptModel.Gpt4o || c.ChatGpt.ModelName.Equals("gpt432k", StringComparison.OrdinalIgnoreCase)
: c.ChatGpt.ModelName; ? "gpt-4o"
: "gpt-3.5-turbo";
#pragma warning restore CS0612 // Type or member is obsolete #pragma warning restore CS0612 // Type or member is obsolete
}); });
} }
if (data.Version < 5)
{
ModifyConfig(c =>
{
c.Version = 5;
c.ChatBot = c.ChatBot == ChatBotImplementation.OpenAi
? ChatBotImplementation.OpenAi
: c.ChatBot;
if (c.ChatGpt.ModelName.Equals("gpt4o", StringComparison.OrdinalIgnoreCase))
{
c.ChatGpt.ModelName = "gpt-4o";
}
else if (c.ChatGpt.ModelName.Equals("gpt35turbo", StringComparison.OrdinalIgnoreCase))
{
c.ChatGpt.ModelName = "gpt-3.5-turbo";
}
else
{
Log.Warning(
"Unknown OpenAI api model name: {ModelName}. "
+ "It will be reset to 'gpt-3.5-turbo' only this time",
c.ChatGpt.ModelName);
c.ChatGpt.ModelName = "gpt-3.5-turbo";
}
});
}
} }
} }