Updated Games module

This commit is contained in:
Toastie 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
using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models;
using EllieBot.Modules.Games.Common;
using EllieBot.Modules.Games.Common.ChatterBot;
using EllieBot.Modules.Patronage;
@ -58,18 +57,21 @@ public class ChatterBotService : IExecOnMessage
Log.Information("Cleverbot will not work as the api key is missing");
return null;
case ChatBotImplementation.Gpt:
case ChatBotImplementation.OpenAi:
var data = _gcs.Data;
if (!string.IsNullOrWhiteSpace(_creds.Gpt3ApiKey))
return new OfficialGptSession(_creds.Gpt3ApiKey,
_gcs.Data.ChatGpt.ModelName,
_gcs.Data.ChatGpt.ChatHistory,
_gcs.Data.ChatGpt.MaxTokens,
_gcs.Data.ChatGpt.MinTokens,
_gcs.Data.ChatGpt.PersonalityPrompt,
return new OpenAiApiSession(
data.ChatGpt.ApiUrl,
_creds.Gpt3ApiKey,
data.ChatGpt.ModelName,
data.ChatGpt.ChatHistory,
data.ChatGpt.MaxTokens,
data.ChatGpt.MinTokens,
data.ChatGpt.PersonalityPrompt,
_client.CurrentUser.Username,
_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;
default:
return null;
@ -88,15 +90,15 @@ public class ChatterBotService : IExecOnMessage
public string PrepareMessage(IUserMessage msg)
{
var ellieId = _client.CurrentUser.Id;
var normalMention = $"<@{ellieId}> ";
var nickMention = $"<@!{ellieId}> ";
var nadekoId = _client.CurrentUser.Id;
var normalMention = $"<@{nadekoId}> ";
var nickMention = $"<@!{nadekoId}> ";
string message;
if (msg.Content.StartsWith(normalMention, StringComparison.InvariantCulture))
message = msg.Content[normalMention.Length..].Trim();
else if (msg.Content.StartsWith(nickMention, StringComparison.InvariantCulture))
message = msg.Content[nickMention.Length..].Trim();
else if (msg.ReferencedMessage?.Author.Id == ellieId)
else if (msg.ReferencedMessage?.Author.Id == nadekoId)
message = msg.Content;
else
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
using Newtonsoft.Json;
using OneOf.Types;
using System.Net.Http.Json;
using SharpToken;
using System.CodeDom;
using System.Net.Http.Json;
using System.Text.RegularExpressions;
namespace EllieBot.Modules.Games.Common.ChatterBot;
public partial class OfficialGptSession : IChatterBotSession
public partial class OpenAiApiSession : IChatterBotSession
{
private string Uri
=> $"https://api.openai.com/v1/chat/completions";
private readonly string _baseUrl;
private readonly string _apiKey;
private readonly string _model;
private readonly int _maxHistory;
private readonly int _maxTokens;
private readonly int _minTokens;
private readonly string _ellieUsername;
private readonly string _nadekoUsername;
private readonly GptEncoding _encoding;
private List<GPTMessage> messages = new();
private List<OpenAiApiMessage> messages = new();
private readonly IHttpClientFactory _httpFactory;
public OfficialGptSession(
public OpenAiApiSession(
string url,
string apiKey,
ChatGptModel model,
string model,
int chatHistory,
int maxTokens,
int minTokens,
string personality,
string ellieUsername,
string nadekoUsername,
IHttpClientFactory factory)
{
_apiKey = apiKey;
_httpFactory = factory;
_model = model switch
if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out _))
{
ChatGptModel.Gpt35Turbo => "gpt-3.5-turbo",
ChatGptModel.Gpt4o => "gpt-4o",
_ => throw new ArgumentException("Unknown, unsupported or obsolete model", nameof(model))
};
throw new ArgumentException("Invalid OpenAi api url provided", nameof(url));
}
_baseUrl = url.TrimEnd('/');
_apiKey = apiKey;
_model = model;
_httpFactory = factory;
_maxHistory = chatHistory;
_maxTokens = maxTokens;
_minTokens = minTokens;
_ellieUsername = UsernameCleaner().Replace(ellieUsername, "");
_encoding = GptEncoding.GetEncodingForModel(_model);
messages.Add(new()
_nadekoUsername = UsernameCleaner().Replace(nadekoUsername, "");
_encoding = GptEncoding.GetEncodingForModel("gpt-4o");
if (!string.IsNullOrWhiteSpace(personality))
{
Role = "system",
Content = personality,
Name = _ellieUsername
});
messages.Add(new()
{
Role = "system",
Content = personality,
Name = _nadekoUsername
});
}
}
[GeneratedRegex("[^a-zA-Z0-9_-]")]
private static partial Regex UsernameCleaner();
public async Task<OneOf.OneOf<ThinkResult, Error<string>>> Think(string input, string username)
{
username = UsernameCleaner().Replace(username, "");
messages.Add(new()
{
Role = "user",
Content = input,
Name = username
});
while (messages.Count > _maxHistory + 2)
{
messages.RemoveAt(1);
@ -92,28 +93,29 @@ public partial class OfficialGptSession : IChatterBotSession
}
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();
http.DefaultRequestHeaders.Authorization = new("Bearer", _apiKey);
var data = await http.PostAsJsonAsync(Uri,
new Gpt3ApiRequest()
var data = await http.PostAsJsonAsync($"{_baseUrl}/v1/chat/completions",
new OpenAiApiRequest()
{
Model = _model,
Messages = messages,
MaxTokens = _maxTokens - tokensUsed,
Temperature = 1,
});
var dataString = await data.Content.ReadAsStringAsync();
try
{
var response = JsonConvert.DeserializeObject<OpenAiCompletionResponse>(dataString);
Log.Information("Received response: {response} ", dataString);
// Log.Information("Received response: {Response} ", dataString);
var res = response?.Choices?[0];
var message = res?.Message?.Content;
@ -121,14 +123,14 @@ public partial class OfficialGptSession : IChatterBotSession
{
return new Error<string>("ChatGpt: Received no response.");
}
messages.Add(new()
{
Role = "assistant",
Content = message,
Name = _ellieUsername
Name = _nadekoUsername
});
return new ThinkResult()
{
Text = message,
@ -142,11 +144,4 @@ public partial class OfficialGptSession : IChatterBotSession
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>
{
[Comment("DO NOT CHANGE")]
public int Version { get; set; } = 4;
public int Version { get; set; } = 5;
[Comment("Hangman related settings (.hangman command)")]
public HangmanConfig Hangman { get; set; } = new()
@ -103,10 +103,13 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
}
];
[Comment(@"Which chatbot API should bot use.
'cleverbot' - bot will use Cleverbot API.
'gpt' - bot will use GPT API")]
public ChatBotImplementation ChatBot { get; set; } = ChatBotImplementation.Gpt;
[Comment(
"""
Which chatbot API should bot use.
'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();
}
@ -114,19 +117,38 @@ public sealed partial class GamesConfig : ICloneable<GamesConfig>
[Cloneable]
public sealed partial class ChatGptConfig
{
[Comment(@"Which GPT Model should bot use.
gpt35turbo - cheapest
gpt4o - more expensive, higher quality
")]
public ChatGptModel ModelName { get; set; } = ChatGptModel.Gpt35Turbo;
[Comment("""
Url to any openai api compatible url.
Make sure to modify the modelName appropriately
DO NOT add /v1/chat/completions suffix to the url
""")]
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)")]
public string PersonalityPrompt { get; set; } = "You are a chat bot willing to have a conversation with anyone about anything.";
[Comment("""
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;
[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;
[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; }
[Comment("""
Users won't be able to start trivia games which have
a smaller win requirement than the one specified by this setting.
""")]
Users won't be able to start trivia games which have
a smaller win requirement than the one specified by this setting.
""")]
public int MinimumWinReq { get; set; } = 1;
}
@ -163,18 +185,11 @@ public sealed partial class RaceAnimal
public enum ChatBotImplementation
{
Cleverbot,
OpenAi = 1,
[Obsolete]
Gpt = 1,
[Obsolete]
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,
ConfigParsers.InsensitiveEnum,
ConfigPrinters.ToString);
AddParsedProp("gpt.apiUrl",
gs => gs.ChatGpt.ApiUrl,
ConfigParsers.String,
ConfigPrinters.ToString);
AddParsedProp("gpt.modelName",
gs => gs.ChatGpt.ModelName,
ConfigParsers.InsensitiveEnum,
ConfigParsers.String,
ConfigPrinters.ToString);
AddParsedProp("gpt.personality",
gs => gs.ChatGpt.PersonalityPrompt,
ConfigParsers.String,
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();
}
@ -78,7 +70,7 @@ public sealed class GamesConfigService : ConfigServiceBase<GamesConfig>
ModifyConfig(c =>
{
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;
#pragma warning disable CS0612 // Type or member is obsolete
c.ChatGpt.ModelName =
c.ChatGpt.ModelName == ChatGptModel.Gpt4 || c.ChatGpt.ModelName == ChatGptModel.Gpt432k
? ChatGptModel.Gpt4o
: c.ChatGpt.ModelName;
c.ChatGpt.ModelName.Equals("gpt4", StringComparison.OrdinalIgnoreCase)
|| c.ChatGpt.ModelName.Equals("gpt432k", StringComparison.OrdinalIgnoreCase)
? "gpt-4o"
: "gpt-3.5-turbo";
#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";
}
});
}
}
}