Merge pull request 'add ai commands' (#1) from ai-commands into main
Reviewed-on: #1
This commit is contained in:
118
Commands/LlmCommands.cs
Normal file
118
Commands/LlmCommands.cs
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
using Discord;
|
||||||
|
using Discord.Interactions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TentacleBot.Services;
|
||||||
|
|
||||||
|
namespace TentacleBot.Commands;
|
||||||
|
|
||||||
|
[Group("llm", "LLM chat commands")]
|
||||||
|
public class LlmCommands : InteractionModuleBase<SocketInteractionContext>
|
||||||
|
{
|
||||||
|
private readonly LlmService _llmService;
|
||||||
|
private readonly ILogger<LlmCommands> _logger;
|
||||||
|
|
||||||
|
public LlmCommands(LlmService llmService, ILogger<LlmCommands> logger)
|
||||||
|
{
|
||||||
|
_llmService = llmService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SlashCommand("chat", "Chat with the LLM model")]
|
||||||
|
public async Task HandleChatAsync(
|
||||||
|
[Summary("prompt", "Your message to the LLM")] string prompt)
|
||||||
|
{
|
||||||
|
await DeferAsync();
|
||||||
|
|
||||||
|
var channelId = Context.Channel.Id;
|
||||||
|
var userName = Context.User.Username;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var accumulated = "";
|
||||||
|
|
||||||
|
await foreach (var text in _llmService.SendMessageAsync(channelId, userName, prompt))
|
||||||
|
{
|
||||||
|
accumulated = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(accumulated))
|
||||||
|
{
|
||||||
|
await FollowupAsync(embed: new EmbedBuilder()
|
||||||
|
.WithTitle("LLM Response")
|
||||||
|
.WithDescription("The model returned an empty response.")
|
||||||
|
.WithColor(Color.Orange)
|
||||||
|
.Build());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accumulated.Length <= 4096)
|
||||||
|
{
|
||||||
|
await FollowupAsync(embed: new EmbedBuilder()
|
||||||
|
.WithTitle($"Response from {Context.Client.CurrentUser?.Username}")
|
||||||
|
.WithDescription(Format.Code(accumulated))
|
||||||
|
.WithColor(Color.DarkTeal)
|
||||||
|
.WithTimestamp(DateTimeOffset.UtcNow)
|
||||||
|
.Build());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var truncated = accumulated.Substring(0, 4096);
|
||||||
|
await FollowupAsync(embed: new EmbedBuilder()
|
||||||
|
.WithTitle("LLM Response (truncated)")
|
||||||
|
.WithDescription($"{Format.Code(truncated)}\n\n*Response exceeded Discord's message limit and was truncated.*")
|
||||||
|
.WithColor(Color.Orange)
|
||||||
|
.WithTimestamp(DateTimeOffset.UtcNow)
|
||||||
|
.Build());
|
||||||
|
|
||||||
|
var remaining = accumulated.Substring(4096);
|
||||||
|
var continuation = "Continued...";
|
||||||
|
|
||||||
|
while (remaining.Length > 0)
|
||||||
|
{
|
||||||
|
var chunk = remaining.Length > 4096 ? remaining.Substring(0, 4096) : remaining;
|
||||||
|
continuation += $"\n\n{Format.Code(chunk)}";
|
||||||
|
remaining = remaining.Substring(4096);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await FollowupAsync(continuation, embed: new EmbedBuilder()
|
||||||
|
.WithTitle("LLM Response (continued)")
|
||||||
|
.WithColor(Color.DarkTeal)
|
||||||
|
.Build());
|
||||||
|
continuation = "";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error sending continuation message");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error in LLM chat command");
|
||||||
|
await FollowupAsync(embed: new EmbedBuilder()
|
||||||
|
.WithTitle("Error")
|
||||||
|
.WithDescription("An error occurred while communicating with the LLM.")
|
||||||
|
.WithColor(Color.Red)
|
||||||
|
.WithTimestamp(DateTimeOffset.UtcNow)
|
||||||
|
.Build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [SlashCommand("reset", "Reset conversation history for this channel")]
|
||||||
|
// public async Task HandleResetAsync()
|
||||||
|
// {
|
||||||
|
// _llmService.ResetHistory(Context.Channel.Id);
|
||||||
|
|
||||||
|
// var embed = new EmbedBuilder()
|
||||||
|
// .WithTitle("History Reset")
|
||||||
|
// .WithDescription("Conversation history for this channel has been cleared.")
|
||||||
|
// .WithColor(Color.Green)
|
||||||
|
// .WithTimestamp(DateTimeOffset.UtcNow)
|
||||||
|
// .Build();
|
||||||
|
|
||||||
|
// await RespondAsync(embed: embed);
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -31,7 +31,8 @@ var services = new ServiceCollection()
|
|||||||
LogLevel = LogSeverity.Info,
|
LogLevel = LogSeverity.Info,
|
||||||
UseCompiledLambda = true,
|
UseCompiledLambda = true,
|
||||||
}))
|
}))
|
||||||
.AddSingleton<BotService>();
|
.AddSingleton<BotService>()
|
||||||
|
.AddSingleton<LlmService>();
|
||||||
|
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -80,17 +80,17 @@ docker compose down
|
|||||||
|
|
||||||
```
|
```
|
||||||
TentacleBot/
|
TentacleBot/
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Program.cs # Entry point and DI setup
|
├── Program.cs # Entry point and DI setup
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Services/
|
├── Services/
|
||||||
<EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> BotService.cs # Core bot service
|
│ └── BotService.cs # Core bot service
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Commands/
|
├── Commands/
|
||||||
<EFBFBD><EFBFBD> <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> GeneralCommands.cs # Example slash commands
|
│ └── GeneralCommands.cs # Example slash commands
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> appsettings.json # Configuration
|
├── appsettings.json # Configuration
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Dockerfile # Container image definition
|
├── Dockerfile # Container image definition
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> docker-compose.yml # Local compose setup
|
├── docker-compose.yml # Local compose setup
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> .env.example # Environment variable template
|
├── .env.example # Environment variable template
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> TentacleBot.csproj # Project file
|
├── TentacleBot.csproj # Project file
|
||||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> README.md # This file
|
└── README.md # This file
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example Commands
|
## Example Commands
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public class BotService
|
|||||||
{
|
{
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly InteractionService _interactionService;
|
private readonly InteractionService _interactionService;
|
||||||
|
private readonly LlmService _llmService;
|
||||||
private readonly ILogger<BotService> _logger;
|
private readonly ILogger<BotService> _logger;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
@@ -17,12 +18,14 @@ public class BotService
|
|||||||
public BotService(
|
public BotService(
|
||||||
DiscordSocketClient client,
|
DiscordSocketClient client,
|
||||||
InteractionService interactionService,
|
InteractionService interactionService,
|
||||||
|
LlmService llmService,
|
||||||
ILogger<BotService> logger,
|
ILogger<BotService> logger,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IServiceProvider serviceProvider)
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
_interactionService = interactionService;
|
_interactionService = interactionService;
|
||||||
|
_llmService = llmService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
@@ -35,6 +38,7 @@ public class BotService
|
|||||||
|
|
||||||
_client.Ready += ClientReadyAsync;
|
_client.Ready += ClientReadyAsync;
|
||||||
_client.InteractionCreated += InteractionCreatedAsync;
|
_client.InteractionCreated += InteractionCreatedAsync;
|
||||||
|
_client.MessageReceived += MessageReceivedAsync;
|
||||||
|
|
||||||
var token = Environment.GetEnvironmentVariable("DISCORD_BOT_TOKEN");
|
var token = Environment.GetEnvironmentVariable("DISCORD_BOT_TOKEN");
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
@@ -87,6 +91,105 @@ public class BotService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task MessageReceivedAsync(SocketMessage socketMessage)
|
||||||
|
{
|
||||||
|
if (socketMessage is not SocketUserMessage message)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (message.Source != MessageSource.User || message.Author.IsBot || message.Author.IsWebhook)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_client.CurrentUser is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var botId = _client.CurrentUser.Id;
|
||||||
|
var hasMention = message.MentionedUsers.Any(u => u.Id == botId);
|
||||||
|
var isReplyToBot = false;
|
||||||
|
|
||||||
|
if (message.Reference?.MessageId.IsSpecified == true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var referenced = message.ReferencedMessage ??
|
||||||
|
await message.Channel.GetMessageAsync(message.Reference.MessageId.Value);
|
||||||
|
|
||||||
|
isReplyToBot = referenced?.Author.Id == botId;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Unable to resolve referenced message for reply detection.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMention && !isReplyToBot)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var prompt = hasMention ? RemoveBotMentions(message.Content, botId) : message.Content.Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(prompt))
|
||||||
|
{
|
||||||
|
await message.ReplyAsync("Please include a prompt after mentioning me.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var accumulated = string.Empty;
|
||||||
|
|
||||||
|
await foreach (var text in _llmService.SendMessageAsync(message.Channel.Id, message.Author.Username, prompt))
|
||||||
|
{
|
||||||
|
accumulated = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(accumulated))
|
||||||
|
{
|
||||||
|
await message.ReplyAsync("The model returned an empty response.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await SendChunkedReplyAsync(message, accumulated);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error handling mention/reply LLM chat in channel {ChannelId}", message.Channel.Id);
|
||||||
|
await message.ReplyAsync("I hit an error while generating a response.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SendChunkedReplyAsync(SocketUserMessage message, string response)
|
||||||
|
{
|
||||||
|
const int maxChunkLength = 2000;
|
||||||
|
|
||||||
|
for (var start = 0; start < response.Length; start += maxChunkLength)
|
||||||
|
{
|
||||||
|
var length = Math.Min(maxChunkLength, response.Length - start);
|
||||||
|
var chunk = response.Substring(start, length);
|
||||||
|
|
||||||
|
if (start == 0)
|
||||||
|
{
|
||||||
|
await message.ReplyAsync(chunk);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await message.Channel.SendMessageAsync(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveBotMentions(string content, ulong botId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var rawMention = $"<@{botId}>";
|
||||||
|
var nickMention = $"<@!{botId}>";
|
||||||
|
|
||||||
|
return content
|
||||||
|
.Replace(rawMention, string.Empty, StringComparison.Ordinal)
|
||||||
|
.Replace(nickMention, string.Empty, StringComparison.Ordinal)
|
||||||
|
.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
private static Task LogAsync(LogMessage message)
|
private static Task LogAsync(LogMessage message)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"[{message.Severity}] {message.Source}: {message.Message}");
|
Console.WriteLine($"[{message.Severity}] {message.Source}: {message.Message}");
|
||||||
|
|||||||
226
Services/LlmService.cs
Normal file
226
Services/LlmService.cs
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace TentacleBot.Services;
|
||||||
|
|
||||||
|
public class LlmService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<LlmService> _logger;
|
||||||
|
private readonly string _endpoint;
|
||||||
|
private readonly string _apiKey;
|
||||||
|
private readonly string _model;
|
||||||
|
private readonly int _maxTokens;
|
||||||
|
private readonly double _temperature;
|
||||||
|
private readonly ConcurrentDictionary<ulong, List<ChatMessage>> _histories = new();
|
||||||
|
private const int MaxHistoryChars = 20000;
|
||||||
|
|
||||||
|
public LlmService(IConfiguration configuration, ILogger<LlmService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_httpClient = new HttpClient();
|
||||||
|
|
||||||
|
_endpoint = configuration["Llm:Endpoint"] ?? throw new InvalidOperationException("Llm:Endpoint is not configured");
|
||||||
|
_apiKey = configuration["Llm:ApiKey"] ?? throw new InvalidOperationException("Llm:ApiKey is not configured");
|
||||||
|
_model = configuration["Llm:Model"] ?? throw new InvalidOperationException("Llm:Model is not configured");
|
||||||
|
_maxTokens = int.Parse(configuration["Llm:MaxTokens"] ?? "1024");
|
||||||
|
_temperature = double.Parse(configuration["Llm:Temperature"] ?? "0.7");
|
||||||
|
|
||||||
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);
|
||||||
|
_httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async IAsyncEnumerable<string> SendMessageAsync(ulong channelId, string user, string message)
|
||||||
|
{
|
||||||
|
var history = _histories.GetOrAdd(channelId, _ => new List<ChatMessage>());
|
||||||
|
|
||||||
|
history.Add(new ChatMessage { Role = "user", Content = message });
|
||||||
|
|
||||||
|
TrimHistory(history);
|
||||||
|
|
||||||
|
var request = new ChatRequest
|
||||||
|
{
|
||||||
|
Model = _model,
|
||||||
|
Messages = history.Select(m => new ChatMessage { Role = m.Role, Content = m.Content }).ToList(),
|
||||||
|
MaxTokens = _maxTokens,
|
||||||
|
Temperature = (float)_temperature,
|
||||||
|
Stream = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(request, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
using var response = await _httpClient.PostAsync(_endpoint, content);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorBody = await response.Content.ReadAsStringAsync();
|
||||||
|
_logger.LogError("LLM API error ({StatusCode}): {Error}", response.StatusCode, errorBody);
|
||||||
|
throw new HttpRequestException($"LLM API returned {response.StatusCode}: {errorBody}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var accumulated = new StringBuilder();
|
||||||
|
|
||||||
|
var mediaType = response.Content.Headers.ContentType?.MediaType;
|
||||||
|
if (string.Equals(mediaType, "text/event-stream", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
|
||||||
|
while (await reader.ReadLineAsync() is { } line)
|
||||||
|
{
|
||||||
|
if (!line.StartsWith("data:", StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var data = line.Substring(5).Trim();
|
||||||
|
if (data == "[DONE]")
|
||||||
|
break;
|
||||||
|
|
||||||
|
var token = TryParseStreamToken(data);
|
||||||
|
if (!string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
accumulated.Append(token);
|
||||||
|
yield return accumulated.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
var fullText = TryParseNonStreamContent(body);
|
||||||
|
if (!string.IsNullOrWhiteSpace(fullText))
|
||||||
|
{
|
||||||
|
accumulated.Append(fullText);
|
||||||
|
yield return accumulated.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accumulated.Length > 0)
|
||||||
|
{
|
||||||
|
history.Add(new ChatMessage { Role = "assistant", Content = accumulated.ToString() });
|
||||||
|
TrimHistory(history);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryParseStreamToken(string data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(data);
|
||||||
|
|
||||||
|
if (doc.RootElement.TryGetProperty("choices", out var choices) &&
|
||||||
|
choices.ValueKind == JsonValueKind.Array &&
|
||||||
|
choices.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var firstChoice = choices[0];
|
||||||
|
|
||||||
|
if (firstChoice.TryGetProperty("delta", out var delta) &&
|
||||||
|
delta.ValueKind == JsonValueKind.Object &&
|
||||||
|
delta.TryGetProperty("content", out var deltaContent) &&
|
||||||
|
deltaContent.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return deltaContent.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstChoice.TryGetProperty("text", out var text) &&
|
||||||
|
text.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return text.GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.RootElement.TryGetProperty("delta", out var rootDelta) &&
|
||||||
|
rootDelta.ValueKind == JsonValueKind.Object &&
|
||||||
|
rootDelta.TryGetProperty("content", out var rootContent) &&
|
||||||
|
rootContent.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return rootContent.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.RootElement.TryGetProperty("content", out var content) &&
|
||||||
|
content.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return content.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryParseNonStreamContent(string body)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(body);
|
||||||
|
|
||||||
|
if (doc.RootElement.TryGetProperty("choices", out var choices) &&
|
||||||
|
choices.ValueKind == JsonValueKind.Array &&
|
||||||
|
choices.GetArrayLength() > 0)
|
||||||
|
{
|
||||||
|
var firstChoice = choices[0];
|
||||||
|
|
||||||
|
if (firstChoice.TryGetProperty("message", out var message) &&
|
||||||
|
message.ValueKind == JsonValueKind.Object &&
|
||||||
|
message.TryGetProperty("content", out var messageContent) &&
|
||||||
|
messageContent.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return messageContent.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstChoice.TryGetProperty("text", out var text) &&
|
||||||
|
text.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return text.GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.RootElement.TryGetProperty("content", out var content) &&
|
||||||
|
content.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return content.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetHistory(ulong channelId)
|
||||||
|
{
|
||||||
|
_histories.TryRemove(channelId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrimHistory(List<ChatMessage> history)
|
||||||
|
{
|
||||||
|
while (history.Sum(m => m.Content.Length) > MaxHistoryChars && history.Count > 2)
|
||||||
|
{
|
||||||
|
history.RemoveAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ChatRequest
|
||||||
|
{
|
||||||
|
public string Model { get; init; } = "";
|
||||||
|
public List<ChatMessage> Messages { get; init; } = new();
|
||||||
|
public int MaxTokens { get; init; }
|
||||||
|
public float Temperature { get; init; }
|
||||||
|
public bool Stream { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ChatMessage
|
||||||
|
{
|
||||||
|
public string Role { get; set; } = "";
|
||||||
|
public string Content { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user