feat: on giving bot a voice
This commit is contained in:
@@ -28,6 +28,7 @@ dotnet_diagnostic.CA1848.severity = none
|
|||||||
dotnet_diagnostic.IDE0100.severity = none
|
dotnet_diagnostic.IDE0100.severity = none
|
||||||
dotnet_diagnostic.IDE0058.severity = none
|
dotnet_diagnostic.IDE0058.severity = none
|
||||||
dotnet_diagnostic.IDE0290.severity = none
|
dotnet_diagnostic.IDE0290.severity = none
|
||||||
|
dotnet_diagnostic.CA1031.severity = none
|
||||||
|
|
||||||
# Organize usings
|
# Organize usings
|
||||||
dotnet_separate_import_directive_groups = true
|
dotnet_separate_import_directive_groups = true
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ public sealed record DiscordMessage
|
|||||||
[JsonPropertyName("author")]
|
[JsonPropertyName("author")]
|
||||||
public DiscordUser Author { get; init; } = new DiscordUser();
|
public DiscordUser Author { get; init; } = new DiscordUser();
|
||||||
|
|
||||||
|
[JsonPropertyName("content")]
|
||||||
|
public string Content { get; init; } = string.Empty;
|
||||||
|
|
||||||
[JsonPropertyName("mentions")]
|
[JsonPropertyName("mentions")]
|
||||||
public IEnumerable<DiscordUser> Mentions { get; init; } = [];
|
public IEnumerable<DiscordUser> Mentions { get; init; } = [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -12,6 +13,11 @@ public sealed class DiscordRestClient : IDiscordRestClient
|
|||||||
{
|
{
|
||||||
private readonly ILogger<DiscordRestClient> _logger;
|
private readonly ILogger<DiscordRestClient> _logger;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
public DiscordRestClient(
|
public DiscordRestClient(
|
||||||
ILogger<DiscordRestClient> logger,
|
ILogger<DiscordRestClient> logger,
|
||||||
@@ -29,18 +35,19 @@ public sealed class DiscordRestClient : IDiscordRestClient
|
|||||||
{
|
{
|
||||||
var gatewayEndpoint = new Uri("gateway", UriKind.Relative);
|
var gatewayEndpoint = new Uri("gateway", UriKind.Relative);
|
||||||
var response = await _httpClient.GetAsync(gatewayEndpoint, cancellationToken);
|
var response = await _httpClient.GetAsync(gatewayEndpoint, cancellationToken);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode is false)
|
if (response.IsSuccessStatusCode is false)
|
||||||
{
|
{
|
||||||
_logger.LogError("Failed to get gateway URL: {StatusCode}", response.StatusCode);
|
_logger.LogError("Failed to get gateway URL: {StatusCode} - {Content}", response.StatusCode, responseContent);
|
||||||
throw new DiscordRestClientException("Failed to get gateway URL.");
|
throw new DiscordRestClientException("Failed to get gateway URL.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var gatewayResponse = await response.Content.ReadFromJsonAsync<GatewayResponse>(cancellationToken);
|
var gatewayResponse = JsonSerializer.Deserialize<GatewayResponse>(responseContent, JsonOptions);
|
||||||
|
|
||||||
if (gatewayResponse is null)
|
if (gatewayResponse is null)
|
||||||
{
|
{
|
||||||
_logger.LogError("Failed to deserialize gateway response.");
|
_logger.LogError("Failed to deserialize gateway response: {Content}", responseContent);
|
||||||
throw new DiscordRestClientException("Failed to deserialize gateway response.");
|
throw new DiscordRestClientException("Failed to deserialize gateway response.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,18 +58,19 @@ public sealed class DiscordRestClient : IDiscordRestClient
|
|||||||
{
|
{
|
||||||
var channelEndpoint = new Uri($"channels/{channelId}/messages", UriKind.Relative);
|
var channelEndpoint = new Uri($"channels/{channelId}/messages", UriKind.Relative);
|
||||||
var response = await _httpClient.PostAsJsonAsync(channelEndpoint, request, cancellationToken);
|
var response = await _httpClient.PostAsJsonAsync(channelEndpoint, request, cancellationToken);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode is false)
|
if (response.IsSuccessStatusCode is false)
|
||||||
{
|
{
|
||||||
_logger.LogError("Failed to create message: {StatusCode}", response.StatusCode);
|
_logger.LogError("Failed to create message: {StatusCode} - {Content}", response.StatusCode, responseContent);
|
||||||
throw new DiscordRestClientException("Failed to create message.");
|
throw new DiscordRestClientException("Failed to create message.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var discordMessage = await response.Content.ReadFromJsonAsync<DiscordMessage>(cancellationToken);
|
var discordMessage = JsonSerializer.Deserialize<DiscordMessage>(responseContent, JsonOptions);
|
||||||
|
|
||||||
if (discordMessage is null)
|
if (discordMessage is null)
|
||||||
{
|
{
|
||||||
_logger.LogError("Failed to deserialize message create response.");
|
_logger.LogError("Failed to deserialize message create response: {Content}", responseContent);
|
||||||
throw new DiscordRestClientException("Failed to deserialize message create response.");
|
throw new DiscordRestClientException("Failed to deserialize message create response.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,18 +81,19 @@ public sealed class DiscordRestClient : IDiscordRestClient
|
|||||||
{
|
{
|
||||||
var meEndpoint = new Uri($"users/@me", UriKind.Relative);
|
var meEndpoint = new Uri($"users/@me", UriKind.Relative);
|
||||||
var response = await _httpClient.GetAsync(meEndpoint, cancellationToken);
|
var response = await _httpClient.GetAsync(meEndpoint, cancellationToken);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode is false)
|
if (response.IsSuccessStatusCode is false)
|
||||||
{
|
{
|
||||||
_logger.LogError("Failed to retrieve current user: {StatusCode}", response.StatusCode);
|
_logger.LogError("Failed to retrieve current user: {StatusCode} - {Content}", response.StatusCode, responseContent);
|
||||||
throw new DiscordRestClientException("Failed to create message.");
|
throw new DiscordRestClientException("Failed to create message.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var discordUser = await response.Content.ReadFromJsonAsync<DiscordUser>(cancellationToken);
|
var discordUser = JsonSerializer.Deserialize<DiscordUser>(responseContent, JsonOptions);
|
||||||
|
|
||||||
if (discordUser is null)
|
if (discordUser is null)
|
||||||
{
|
{
|
||||||
_logger.LogError("Failed to deserialize user response.");
|
_logger.LogError("Failed to deserialize user response: {Content}", responseContent);
|
||||||
throw new DiscordRestClientException("Failed to deserialize user response.");
|
throw new DiscordRestClientException("Failed to deserialize user response.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
using StevesBot.Library.Discord.Common;
|
using StevesBot.Library.Discord.Common;
|
||||||
using StevesBot.Library.Discord.Rest;
|
using StevesBot.Library.Discord.Rest;
|
||||||
using StevesBot.Library.Gemini;
|
|
||||||
using StevesBot.Library.Telemetry;
|
using StevesBot.Library.Telemetry;
|
||||||
|
|
||||||
namespace StevesBot.Library.Discord;
|
namespace StevesBot.Library.Discord;
|
||||||
@@ -28,16 +27,4 @@ public static class ServicesExtensions
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddGeminiClient(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services
|
|
||||||
.AddHttpClient<IGeminiClient, GeminiClient>(static (sp, c) =>
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
})
|
|
||||||
.AddStandardResilienceHandler();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -7,21 +9,62 @@ public sealed class GeminiClient : IGeminiClient
|
|||||||
{
|
{
|
||||||
private readonly ILogger<GeminiClient> _logger;
|
private readonly ILogger<GeminiClient> _logger;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly GeminiClientOptions _options;
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
};
|
||||||
|
|
||||||
public GeminiClient(
|
public GeminiClient(
|
||||||
ILogger<GeminiClient> logger,
|
ILogger<GeminiClient> logger,
|
||||||
HttpClient httpClient
|
HttpClient httpClient,
|
||||||
|
GeminiClientOptions options
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(logger, nameof(logger));
|
ArgumentNullException.ThrowIfNull(logger, nameof(logger));
|
||||||
ArgumentNullException.ThrowIfNull(httpClient, nameof(httpClient));
|
ArgumentNullException.ThrowIfNull(httpClient, nameof(httpClient));
|
||||||
|
ArgumentNullException.ThrowIfNull(options, nameof(options));
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
|
_options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string> GenerateContentAsync(string input, CancellationToken ct)
|
public async Task<string> GenerateContentAsync(string input, CancellationToken ct)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
const string errorMessage = "Oh boi...I'm not sure what happened. I can't seem to respond right now.";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var requestUri = new Uri($"/v1beta/models/{_options.ModelId}:generateContent", UriKind.Relative);
|
||||||
|
var request = Request.From(input);
|
||||||
|
var json = JsonSerializer.Serialize(request, JsonOptions);
|
||||||
|
using var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
using var response = await _httpClient.PostAsync(requestUri, content, ct);
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync(ct);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Request to generate content failed: {StatusCode} - {Content}", response.StatusCode, responseContent);
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
var geminiResponse = JsonSerializer.Deserialize<Response>(responseContent, JsonOptions);
|
||||||
|
|
||||||
|
if (geminiResponse is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Unable to deserialize content from response: {ResponseContent}", responseContent);
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return geminiResponse.GetText();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Failed to generate content");
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace StevesBot.Library.Gemini;
|
||||||
|
|
||||||
|
public sealed class GeminiClientOptions
|
||||||
|
{
|
||||||
|
public string ApiUrl { get; init; } = string.Empty;
|
||||||
|
public string ModelId { get; init; } = string.Empty;
|
||||||
|
public string ApiKey { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -2,5 +2,6 @@ namespace StevesBot.Library.Gemini;
|
|||||||
|
|
||||||
internal sealed record GenerationConfig
|
internal sealed record GenerationConfig
|
||||||
{
|
{
|
||||||
public double? Temperature { get; init; }
|
public double Temperature { get; init; } = 1.0;
|
||||||
|
public int MaxOutputTokens { get; init; } = 500;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,26 @@
|
|||||||
namespace StevesBot.Library.Gemini;
|
namespace StevesBot.Library.Gemini;
|
||||||
|
|
||||||
internal sealed record GeminiRequest
|
internal sealed record Request
|
||||||
{
|
{
|
||||||
public Content? SystemInstruction { get; init; }
|
public Content SystemInstruction { get; init; } = new();
|
||||||
public required Content[] Contents { get; init; }
|
public Content[] Contents { get; init; } = [];
|
||||||
public GenerationConfig? GenerationConfig { get; init; }
|
public GenerationConfig GenerationConfig { get; init; } = new();
|
||||||
|
|
||||||
|
public static Request From(string text)
|
||||||
|
{
|
||||||
|
return new Request()
|
||||||
|
{
|
||||||
|
Contents = [
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Parts = [
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Text = text,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace StevesBot.Library.Gemini;
|
||||||
|
|
||||||
|
public static class ServicesExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddGeminiClient(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddOptions<GeminiClientOptions>()
|
||||||
|
.BindConfiguration(nameof(GeminiClientOptions));
|
||||||
|
|
||||||
|
services.AddSingleton(static sp =>
|
||||||
|
{
|
||||||
|
var geminiOptions = sp.GetRequiredService<IOptions<GeminiClientOptions>>().Value;
|
||||||
|
return geminiOptions;
|
||||||
|
});
|
||||||
|
|
||||||
|
services
|
||||||
|
.AddHttpClient<IGeminiClient, GeminiClient>(static (sp, c) =>
|
||||||
|
{
|
||||||
|
var geminiOptions = sp.GetRequiredService<GeminiClientOptions>();
|
||||||
|
c.BaseAddress = new Uri(geminiOptions.ApiUrl);
|
||||||
|
c.DefaultRequestHeaders.Add("x-goog-api-key", geminiOptions.ApiKey);
|
||||||
|
})
|
||||||
|
.AddStandardResilienceHandler();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,10 +31,10 @@ internal static class TaggedMessageHandler
|
|||||||
|
|
||||||
logger.LogInformation("Bot tagged in message");
|
logger.LogInformation("Bot tagged in message");
|
||||||
|
|
||||||
// TODO: Get message content from discord
|
var llmResponse = await geminiClient.GenerateContentAsync(mcde.Data.Content, cancellationToken);
|
||||||
// event
|
|
||||||
var llmResponse = await geminiClient.GenerateContentAsync("Hello", cancellationToken);
|
|
||||||
|
|
||||||
|
// TODO: LLM can be wordy...discord has 2000 character limit
|
||||||
|
// on message size. Need to handle that.
|
||||||
var request = new CreateMessageRequest(
|
var request = new CreateMessageRequest(
|
||||||
Content: llmResponse,
|
Content: llmResponse,
|
||||||
MessageReference: new(
|
MessageReference: new(
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ builder.Services.AddSingleton(TimeProvider.System);
|
|||||||
|
|
||||||
builder.Services.AddDiscordRestClient();
|
builder.Services.AddDiscordRestClient();
|
||||||
|
|
||||||
|
builder.Services.AddGeminiClient();
|
||||||
|
|
||||||
builder.Services.AddDiscordGatewayClient(static (client) =>
|
builder.Services.AddDiscordGatewayClient(static (client) =>
|
||||||
{
|
{
|
||||||
client.On(DiscordEventTypes.MessageCreate, WelcomeMessageHandler.HandleAsync);
|
client.On(DiscordEventTypes.MessageCreate, WelcomeMessageHandler.HandleAsync);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
global using System.Net;
|
|
||||||
global using System.Net.WebSockets;
|
global using System.Net.WebSockets;
|
||||||
global using System.Reflection;
|
global using System.Reflection;
|
||||||
global using System.Text;
|
global using System.Text;
|
||||||
@@ -11,6 +10,7 @@ global using StevesBot.Library.Discord;
|
|||||||
global using StevesBot.Library.Discord.Common;
|
global using StevesBot.Library.Discord.Common;
|
||||||
global using StevesBot.Library.Discord.Rest;
|
global using StevesBot.Library.Discord.Rest;
|
||||||
global using StevesBot.Library.Discord.Rest.Requests;
|
global using StevesBot.Library.Discord.Rest.Requests;
|
||||||
|
global using StevesBot.Library.Gemini;
|
||||||
global using StevesBot.Library.Telemetry;
|
global using StevesBot.Library.Telemetry;
|
||||||
global using StevesBot.Worker;
|
global using StevesBot.Worker;
|
||||||
global using StevesBot.Worker.Discord;
|
global using StevesBot.Worker.Discord;
|
||||||
|
|||||||
@@ -14,5 +14,10 @@
|
|||||||
"ServerUrl": "ServerUrl",
|
"ServerUrl": "ServerUrl",
|
||||||
"ApiKeyHeader": "ApiKeyHeader",
|
"ApiKeyHeader": "ApiKeyHeader",
|
||||||
"ApiKey": "ApiKey"
|
"ApiKey": "ApiKey"
|
||||||
|
},
|
||||||
|
"GeminiClientOptions": {
|
||||||
|
"ApiUrl": "ApiUrl",
|
||||||
|
"ModelId": "ModelId",
|
||||||
|
"ApiKey": "ApiKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user