Refactored logging into a unified logging

This commit is contained in:
tsommer committed 2024-08-25 21:55:34 +02:00
1 parent 200b87f47a
commit 82f7329a39
24 files changed
+416 -224

No files matched your search

+4 -2
View File
@@ -7,13 +7,15 @@ using AIStudio.Tools;
namespace AIStudio.Agents;
public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent
{
protected SettingsManager SettingsManager { get; init; } = settingsManager;
protected IJSRuntime JsRuntime { get; init; } = jsRuntime;
protected ThreadSafeRandom RNG { get; init; } = rng;
protected ILogger<AgentBase> Logger { get; init; } = logger;
/// <summary>
/// Represents the type or category of this agent.
@@ -104,6 +106,6 @@ public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRu
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread);
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread);
}
}
@@ -4,7 +4,7 @@ using AIStudio.Tools;
namespace AIStudio.Agents;
public sealed class AgentTextContentCleaner(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(settingsManager, jsRuntime, rng)
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, jsRuntime, rng)
{
private static readonly ContentBlock EMPTY_BLOCK = new()
{
@@ -10,7 +10,7 @@ namespace AIStudio.Assistants;
public abstract partial class AssistantBase : ComponentBase
{
[Inject]
protected SettingsManager SettingsManager { get; set; } = null!;
protected SettingsManager SettingsManager { get; init; } = null!;
[Inject]
protected IJSRuntime JsRuntime { get; init; } = null!;
@@ -27,6 +27,9 @@ public abstract partial class AssistantBase : ComponentBase
[Inject]
protected NavigationManager NavigationManager { get; init; } = null!;
[Inject]
protected ILogger<AssistantBase> Logger { get; init; } = null!;
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
internal const string RESULT_DIV_ID = "assistantResult";
@@ -151,7 +154,7 @@ public abstract partial class AssistantBase : ComponentBase
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
this.isProcessing = false;
this.StateHasChanged();
@@ -24,6 +24,9 @@ public partial class Workspaces : ComponentBase
[Inject]
private ThreadSafeRandom RNG { get; init; } = null!;
[Inject]
private ILogger<Workspaces> Logger { get; init; } = null!;
[Parameter]
public ChatThread? CurrentChatThread { get; set; }
@@ -309,7 +312,7 @@ public partial class Workspaces : ComponentBase
}
catch (Exception e)
{
Console.WriteLine(e);
this.Logger.LogError($"Failed to load chat from '{chatPath}': {e.Message}");
}
return null;
+6 -3
View File
@@ -19,7 +19,7 @@ namespace AIStudio.Pages;
public partial class Chat : MSGComponentBase, IAsyncDisposable
{
[Inject]
private SettingsManager SettingsManager { get; set; } = null!;
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
public IJSRuntime JsRuntime { get; init; } = null!;
@@ -28,7 +28,10 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
private ThreadSafeRandom RNG { get; init; } = null!;
[Inject]
public IDialogService DialogService { get; set; } = null!;
private IDialogService DialogService { get; init; } = null!;
[Inject]
private ILogger<Chat> Logger { get; init; } = null!;
private InnerScrolling scrollingArea = null!;
@@ -189,7 +192,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
// Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
@@ -14,16 +14,19 @@ namespace AIStudio.Pages;
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
{
[Inject]
public SettingsManager SettingsManager { get; init; } = null!;
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
public IDialogService DialogService { get; init; } = null!;
private IDialogService DialogService { get; init; } = null!;
[Inject]
public IJSRuntime JsRuntime { get; init; } = null!;
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
protected MessageBus MessageBus { get; init; } = null!;
private MessageBus MessageBus { get; init; } = null!;
[Inject]
private ILogger<Settings> Logger { get; init; } = null!;
private readonly List<ConfigurationSelectData<string>> availableProviders = new();
@@ -111,7 +114,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
if (dialogResult is null || dialogResult.Canceled)
return;
var providerInstance = provider.CreateProvider();
var providerInstance = provider.CreateProvider(this.Logger);
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
if(deleteSecretResponse.Success)
{
+17
View File
@@ -4,6 +4,8 @@ using AIStudio.Settings;
using AIStudio.Tools;
using AIStudio.Tools.Services;
using Microsoft.Extensions.Logging.Console;
using MudBlazor.Services;
#if !DEBUG
@@ -35,6 +37,18 @@ if(string.IsNullOrWhiteSpace(secretKey))
}
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
builder.Logging.SetMinimumLevel(LogLevel.Debug);
builder.Logging.AddFilter("Microsoft", LogLevel.Information);
builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning);
builder.Logging.AddFilter("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogLevel.Warning);
builder.Logging.AddFilter("Microsoft.AspNetCore.StaticFiles", LogLevel.Warning);
builder.Logging.AddFilter("MudBlazor", LogLevel.Information);
builder.Logging.AddConsole(options =>
{
options.FormatterName = TerminalLogger.FORMATTER_NAME;
}).AddConsoleFormatter<TerminalLogger, ConsoleFormatterOptions>();
builder.Services.AddMudServices(config =>
{
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
@@ -101,5 +115,8 @@ app.MapRazorComponents<App>()
var serverTask = app.RunAsync();
var rustLogger = app.Services.GetRequiredService<ILogger<Rust>>();
rust.SetLogger(rustLogger);
await rust.AppIsReady();
await serverTask;
@@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Anthropic;
public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.com/v1/"), IProvider
public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://api.anthropic.com/v1/", logger), IProvider
{
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
@@ -9,13 +9,21 @@ public abstract class BaseProvider
/// The HTTP client to use for all requests.
/// </summary>
protected readonly HttpClient httpClient = new();
/// <summary>
/// The logger to use.
/// </summary>
protected readonly ILogger logger;
/// <summary>
/// Constructor for the base provider.
/// </summary>
/// <param name="url">The base URL for the provider.</param>
protected BaseProvider(string url)
/// <param name="loggerService">The logger service to use.</param>
protected BaseProvider(string url, ILogger loggerService)
{
this.logger = loggerService;
// Set the base URL:
this.httpClient.BaseAddress = new(url);
}
@@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Fireworks;
public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/inference/v1/"), IProvider
public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.fireworks.ai/inference/v1/", logger), IProvider
{
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
@@ -9,7 +9,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.Mistral;
public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/"), IProvider
public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.mistral.ai/v1/", logger), IProvider
{
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
@@ -11,7 +11,7 @@ namespace AIStudio.Provider.OpenAI;
/// <summary>
/// The OpenAI provider.
/// </summary>
public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"), IProvider
public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.openai.com/v1/", logger), IProvider
{
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
@@ -1,9 +1,3 @@
using AIStudio.Provider.Anthropic;
using AIStudio.Provider.Fireworks;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
namespace AIStudio.Provider;
/// <summary>
@@ -20,59 +14,4 @@ public enum Providers
FIREWORKS = 5,
SELF_HOSTED = 4,
}
/// <summary>
/// Extension methods for the provider enum.
/// </summary>
public static class ExtensionsProvider
{
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
/// <param name="provider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns>
public static string ToName(this Providers provider) => provider switch
{
Providers.NONE => "No provider selected",
Providers.OPEN_AI => "OpenAI",
Providers.ANTHROPIC => "Anthropic",
Providers.MISTRAL => "Mistral",
Providers.FIREWORKS => "Fireworks.ai",
Providers.SELF_HOSTED => "Self-hosted",
_ => "Unknown",
};
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="providerSettings">The provider settings.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this Settings.Provider providerSettings)
{
try
{
return providerSettings.UsedProvider switch
{
Providers.OPEN_AI => new ProviderOpenAI { InstanceName = providerSettings.InstanceName },
Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = providerSettings.InstanceName },
Providers.MISTRAL => new ProviderMistral { InstanceName = providerSettings.InstanceName },
Providers.FIREWORKS => new ProviderFireworks { InstanceName = providerSettings.InstanceName },
Providers.SELF_HOSTED => new ProviderSelfHosted(providerSettings) { InstanceName = providerSettings.InstanceName },
_ => new NoProvider(),
};
}
catch (Exception e)
{
Console.WriteLine($"Failed to create provider: {e.Message}");
return new NoProvider();
}
}
}
@@ -0,0 +1,60 @@
using AIStudio.Provider.Anthropic;
using AIStudio.Provider.Fireworks;
using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
namespace AIStudio.Provider;
public static class ProvidersExtensions
{
/// <summary>
/// Returns the human-readable name of the provider.
/// </summary>
/// <param name="provider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns>
public static string ToName(this Providers provider) => provider switch
{
Providers.NONE => "No provider selected",
Providers.OPEN_AI => "OpenAI",
Providers.ANTHROPIC => "Anthropic",
Providers.MISTRAL => "Mistral",
Providers.FIREWORKS => "Fireworks.ai",
Providers.SELF_HOSTED => "Self-hosted",
_ => "Unknown",
};
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="providerSettings">The provider settings.</param>
/// <param name="logger">The logger to use.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger)
{
try
{
return providerSettings.UsedProvider switch
{
Providers.OPEN_AI => new ProviderOpenAI(logger) { InstanceName = providerSettings.InstanceName },
Providers.ANTHROPIC => new ProviderAnthropic(logger) { InstanceName = providerSettings.InstanceName },
Providers.MISTRAL => new ProviderMistral(logger) { InstanceName = providerSettings.InstanceName },
Providers.FIREWORKS => new ProviderFireworks(logger) { InstanceName = providerSettings.InstanceName },
Providers.SELF_HOSTED => new ProviderSelfHosted(logger, providerSettings) { InstanceName = providerSettings.InstanceName },
_ => new NoProvider(),
};
}
catch (Exception e)
{
logger.LogError($"Failed to create provider: {e.Message}");
return new NoProvider();
}
}
}
@@ -8,7 +8,7 @@ using AIStudio.Settings;
namespace AIStudio.Provider.SelfHosted;
public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}"), IProvider
public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}", logger), IProvider
{
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
@@ -162,7 +162,7 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide
}
catch(Exception e)
{
Console.WriteLine($"Failed to load text models from self-hosted provider: {e.Message}");
this.logger.LogError($"Failed to load text models from self-hosted provider: {e.Message}");
return [];
}
}
@@ -71,10 +71,13 @@ public partial class ProviderDialog : ComponentBase
public bool IsEditing { get; init; }
[Inject]
private SettingsManager SettingsManager { get; set; } = null!;
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; set; } = null!;
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private ILogger<ProviderDialog> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
@@ -133,7 +136,7 @@ public partial class ProviderDialog : ComponentBase
}
var loadedProviderSettings = this.CreateProviderSettings();
var provider = loadedProviderSettings.CreateProvider();
var provider = loadedProviderSettings.CreateProvider(this.Logger);
if(provider is NoProvider)
{
await base.OnInitializedAsync();
@@ -187,7 +190,7 @@ public partial class ProviderDialog : ComponentBase
if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED)
{
// We need to instantiate the provider to store the API key:
var provider = addedProviderSettings.CreateProvider();
var provider = addedProviderSettings.CreateProvider(this.Logger);
// Store the API key in the OS secure storage:
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
@@ -318,7 +321,7 @@ public partial class ProviderDialog : ComponentBase
private async Task ReloadModels()
{
var currentProviderSettings = this.CreateProviderSettings();
var provider = currentProviderSettings.CreateProvider();
var provider = currentProviderSettings.CreateProvider(this.Logger);
if(provider is NoProvider)
return;
@@ -11,7 +11,7 @@ namespace AIStudio.Settings;
/// <summary>
/// The settings manager.
/// </summary>
public sealed class SettingsManager
public sealed class SettingsManager(ILogger<SettingsManager> logger)
{
private const string SETTINGS_FILENAME = "settings.json";
@@ -20,6 +20,8 @@ public sealed class SettingsManager
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
private ILogger<SettingsManager> logger = logger;
/// <summary>
/// The directory where the configuration files are stored.
@@ -102,12 +104,18 @@ public sealed class SettingsManager
public async Task LoadSettings()
{
if(!this.IsSetUp)
{
this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet.");
return;
}
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
if(!File.Exists(settingsPath))
{
this.logger.LogWarning("Cannot load settings, because the settings file does not exist.");
return;
}
// We read the `"Version": "V3"` line to determine the version of the settings file:
await foreach (var line in File.ReadLinesAsync(settingsPath))
{
@@ -123,16 +131,16 @@ public sealed class SettingsManager
Enum.TryParse(settingsVersionText, out Version settingsVersion);
if(settingsVersion is Version.UNKNOWN)
{
Console.WriteLine("Error: Unknown version of the settings file.");
this.logger.LogError("Unknown version of the settings file found.");
this.ConfigurationData = new();
return;
}
this.ConfigurationData = SettingsMigrations.Migrate(settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
this.ConfigurationData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
return;
}
Console.WriteLine("Error: Failed to read the version of the settings file.");
this.logger.LogError("Failed to read the version of the settings file.");
this.ConfigurationData = new();
}
@@ -142,14 +150,22 @@ public sealed class SettingsManager
public async Task StoreSettings()
{
if(!this.IsSetUp)
{
this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
return;
}
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
if(!Directory.Exists(ConfigDirectory))
{
this.logger.LogInformation("Creating the configuration directory.");
Directory.CreateDirectory(ConfigDirectory!);
}
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
await File.WriteAllTextAsync(settingsPath, settingsJson);
this.logger.LogInformation("Stored the settings to the file system.");
}
public void InjectSpellchecking(Dictionary<string, object?> attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false";
@@ -9,7 +9,7 @@ namespace AIStudio.Settings;
public static class SettingsMigrations
{
public static Data Migrate(Version previousVersion, string configData, JsonSerializerOptions jsonOptions)
public static Data Migrate(ILogger<SettingsManager> logger, Version previousVersion, string configData, JsonSerializerOptions jsonOptions)
{
switch (previousVersion)
{
@@ -17,41 +17,41 @@ public static class SettingsMigrations
var configV1 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
if (configV1 is null)
{
Console.WriteLine("Error: failed to parse the configuration. Using default values.");
logger.LogError("Failed to parse the v1 configuration. Using default values.");
return new();
}
configV1 = MigrateV1ToV2(configV1);
configV1 = MigrateV2ToV3(configV1);
return MigrateV3ToV4(configV1);
configV1 = MigrateV1ToV2(logger, configV1);
configV1 = MigrateV2ToV3(logger, configV1);
return MigrateV3ToV4(logger, configV1);
case Version.V2:
var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
if (configV2 is null)
{
Console.WriteLine("Error: failed to parse the configuration. Using default values.");
logger.LogError("Failed to parse the v2 configuration. Using default values.");
return new();
}
configV2 = MigrateV2ToV3(configV2);
return MigrateV3ToV4(configV2);
configV2 = MigrateV2ToV3(logger, configV2);
return MigrateV3ToV4(logger, configV2);
case Version.V3:
var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
if (configV3 is null)
{
Console.WriteLine("Error: failed to parse the configuration. Using default values.");
logger.LogError("Failed to parse the v3 configuration. Using default values.");
return new();
}
return MigrateV3ToV4(configV3);
return MigrateV3ToV4(logger, configV3);
default:
Console.WriteLine("No configuration migration needed.");
logger.LogInformation("No configuration migration is needed.");
var configV4 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
if (configV4 is null)
{
Console.WriteLine("Error: failed to parse the configuration. Using default values.");
logger.LogError("Failed to parse the v4 configuration. Using default values.");
return new();
}
@@ -59,14 +59,14 @@ public static class SettingsMigrations
}
}
private static DataV1V3 MigrateV1ToV2(DataV1V3 previousData)
private static DataV1V3 MigrateV1ToV2(ILogger<SettingsManager> logger, DataV1V3 previousData)
{
//
// Summary:
// In v1 we had no self-hosted providers. Thus, we had no hostnames.
//
Console.WriteLine("Migrating from v1 to v2...");
logger.LogInformation("Migrating from v1 to v2...");
return new()
{
Version = Version.V2,
@@ -81,14 +81,14 @@ public static class SettingsMigrations
};
}
private static DataV1V3 MigrateV2ToV3(DataV1V3 previousData)
private static DataV1V3 MigrateV2ToV3(ILogger<SettingsManager> logger, DataV1V3 previousData)
{
//
// Summary:
// In v2, self-hosted providers had no host (LM Studio, llama.cpp, ollama, etc.)
//
Console.WriteLine("Migrating from v2 to v3...");
logger.LogInformation("Migrating from v2 to v3...");
return new()
{
Version = Version.V3,
@@ -110,14 +110,14 @@ public static class SettingsMigrations
};
}
private static Data MigrateV3ToV4(DataV1V3 previousConfig)
private static Data MigrateV3ToV4(ILogger<SettingsManager> logger, DataV1V3 previousConfig)
{
//
// Summary:
// We grouped the settings into different categories.
//
Console.WriteLine("Migrating from v3 to v4...");
logger.LogInformation("Migrating from v3 to v4...");
return new()
{
Version = Version.V4,
+11 -4
View File
@@ -9,6 +9,13 @@ public sealed class Rust(string apiPort) : IDisposable
{
BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"),
};
private ILogger<Rust>? logger;
public void SetLogger(ILogger<Rust> logService)
{
this.logger = logService;
}
public async Task<int> GetAppPort()
{
@@ -36,14 +43,14 @@ public sealed class Rust(string apiPort) : IDisposable
var response = await initialHttp.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($" Try {tris}/{MAX_TRIES}");
Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime");
await Task.Delay(wait4Try);
continue;
}
var appPortContent = await response.Content.ReadAsStringAsync();
var appPort = int.Parse(appPortContent);
Console.WriteLine($" Received app port from Rust runtime: '{appPort}'");
Console.WriteLine($"Received app port from Rust runtime: '{appPort}'");
return appPort;
}
@@ -54,11 +61,11 @@ public sealed class Rust(string apiPort) : IDisposable
public async Task AppIsReady()
{
const string URL = "/system/dotnet/ready";
Console.WriteLine($"Notifying Rust runtime that the app is ready.");
this.logger!.LogInformation("Notifying Rust runtime that the app is ready.");
var response = await this.http.PostAsync(URL, new StringContent(string.Empty));
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
}
}
@@ -15,7 +15,7 @@ public sealed class MarkdownClipboardService(Rust rust, IJSRuntime jsRuntime, IS
private Rust Rust { get; } = rust;
/// <summary>
/// Gets called when the user wants to copy the markdown to the clipboard.
/// Gets called when the user wants to copy the Markdown to the clipboard.
/// </summary>
/// <param name="text">The Markdown text to copy.</param>
public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, text);
@@ -3,11 +3,13 @@ using AIStudio.Settings.DataModel;
namespace AIStudio.Tools.Services;
public class TemporaryChatService(SettingsManager settingsManager) : BackgroundService
public class TemporaryChatService(ILogger<TemporaryChatService> logger, SettingsManager settingsManager) : BackgroundService
{
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1);
private static bool IS_INITIALIZED;
private readonly ILogger<TemporaryChatService> logger = logger;
#region Overrides of BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -15,10 +17,12 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
this.logger.LogInformation("The temporary chat maintenance service was initialized.");
await settingsManager.LoadSettings();
if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE)
{
Console.WriteLine("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service.");
this.logger.LogWarning("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service.");
return;
}
@@ -34,10 +38,14 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
private Task StartMaintenance()
{
this.logger.LogInformation("Starting maintenance of temporary chat storage.");
var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats");
if(!Directory.Exists(temporaryDirectories))
{
this.logger.LogWarning("Temporary chat storage directory does not exist. End maintenance.");
return Task.CompletedTask;
}
foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories))
{
var chatPath = Path.Join(tempChatDirPath, "thread.json");
@@ -59,9 +67,13 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
};
if(deleteChat)
{
this.logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy.");
Directory.Delete(tempChatDirPath, true);
}
}
this.logger.LogInformation("Finished maintenance of temporary chat storage.");
return Task.CompletedTask;
}
@@ -0,0 +1,23 @@
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
namespace AIStudio.Tools;
public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME)
{
public const string FORMATTER_NAME = "AI Studio Terminal Logger";
public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
{
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
var timestamp = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff");
var logLevel = logEntry.LogLevel.ToString();
var category = logEntry.Category;
textWriter.Write($"=> {timestamp} [{logLevel}] {category}: {message}");
if (logEntry.Exception is not null)
textWriter.Write($" Exception was = {logEntry.Exception}");
textWriter.WriteLine();
}
}