Migrated the get secret calls from Tauri JS to the runtime API

This commit is contained in:
Thorsten Sommer 2024-08-28 21:00:19 +02:00
parent 273376ad97
commit 97854e7eaa
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
53 changed files with 299 additions and 243 deletions

View File

@ -1,13 +1,14 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools;
using RustService = AIStudio.Tools.RustService;
// ReSharper disable MemberCanBePrivate.Global
namespace AIStudio.Agents;
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent
public abstract class AgentBase(ILogger<AgentBase> logger, RustService rustService, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent
{
protected SettingsManager SettingsManager { get; init; } = settingsManager;
@ -17,6 +18,8 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
protected ILogger<AgentBase> Logger { get; init; } = logger;
protected RustService RustService { get; init; } = rustService;
/// <summary>
/// Represents the type or category of this agent.
/// </summary>
@ -106,6 +109,6 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
// 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.Logger), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread);
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger, this.RustService), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread);
}
}

View File

@ -1,10 +1,11 @@
using AIStudio.Chat;
using AIStudio.Settings;
using AIStudio.Tools;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Agents;
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, jsRuntime, rng)
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, RustService rustService, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(logger, rustService, settingsManager, jsRuntime, rng)
{
private static readonly ContentBlock EMPTY_BLOCK = new()
{

View File

@ -1,7 +1,6 @@
using System.Text;
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.Agenda;

View File

@ -1,10 +1,11 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Assistants;
public abstract partial class AssistantBase : ComponentBase
@ -22,7 +23,7 @@ public abstract partial class AssistantBase : ComponentBase
protected ISnackbar Snackbar { get; init; } = null!;
[Inject]
protected Rust Rust { get; init; } = null!;
protected RustService RustService { get; init; } = null!;
[Inject]
protected NavigationManager NavigationManager { get; init; } = null!;
@ -154,7 +155,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.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger, this.RustService), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
this.isProcessing = false;
this.StateHasChanged();
@ -165,7 +166,7 @@ public abstract partial class AssistantBase : ComponentBase
protected async Task CopyToClipboard()
{
await this.Rust.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
}
private static string? GetButtonIcon(string icon)

View File

@ -1,7 +1,5 @@
using System.Text;
using AIStudio.Tools;
namespace AIStudio.Assistants.Coding;
public partial class AssistantCoding : AssistantBaseCore

View File

@ -1,7 +1,6 @@
using System.Text;
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.EMail;

View File

@ -1,5 +1,4 @@
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.GrammarSpelling;

View File

@ -1,5 +1,3 @@
using AIStudio.Tools;
namespace AIStudio.Assistants.IconFinder;
public partial class AssistantIconFinder : AssistantBaseCore

View File

@ -1,5 +1,3 @@
using AIStudio.Tools;
namespace AIStudio.Assistants.LegalCheck;
public partial class AssistantLegalCheck : AssistantBaseCore

View File

@ -1,5 +1,4 @@
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.RewriteImprove;

View File

@ -1,5 +1,4 @@
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.TextSummarizer;

View File

@ -1,5 +1,4 @@
using AIStudio.Chat;
using AIStudio.Tools;
namespace AIStudio.Assistants.Translation;

View File

@ -1,7 +1,7 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Chat;
/// <summary>
@ -40,7 +40,7 @@ public partial class ContentBlockComponent : ComponentBase
public string Class { get; set; } = string.Empty;
[Inject]
private Rust Rust { get; init; } = null!;
private RustService RustService { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
@ -100,7 +100,7 @@ public partial class ContentBlockComponent : ComponentBase
{
case ContentType.TEXT:
var textContent = (ContentText) this.Content;
await this.Rust.CopyText2Clipboard(this.Snackbar, textContent.Text);
await this.RustService.CopyText2Clipboard(this.Snackbar, textContent.Text);
break;
default:

View File

@ -1,5 +1,4 @@
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;

View File

@ -1,5 +1,4 @@
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;

View File

@ -1,5 +1,4 @@
using AIStudio.Layout;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;

View File

@ -1,5 +1,3 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;

View File

@ -1,5 +1,3 @@
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;

View File

@ -1,7 +1,6 @@
using AIStudio.Agents;
using AIStudio.Chat;
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;

View File

@ -5,7 +5,6 @@ using System.Text.Json.Serialization;
using AIStudio.Chat;
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;

View File

@ -1,6 +1,6 @@
using System.Reflection;
using AIStudio.Tools;
using AIStudio.Tools.Rust;
using Microsoft.AspNetCore.Components;

View File

@ -1,5 +1,7 @@
// Global using directives
global using AIStudio.Tools;
global using Microsoft.JSInterop;
global using MudBlazor;

View File

@ -1,13 +1,14 @@
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Layout;
@ -26,7 +27,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
private IDialogService DialogService { get; init; } = null!;
[Inject]
private Rust Rust { get; init; } = null!;
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
@ -189,7 +190,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
this.performingUpdate = true;
this.StateHasChanged();
await this.Rust.InstallUpdate(this.JsRuntime);
await this.RustService.InstallUpdate();
}
private async ValueTask OnLocationChanging(LocationChangingContext context)

View File

@ -1,7 +1,5 @@
using System.Reflection;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Pages;

View File

@ -4,12 +4,12 @@ using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Pages;
@ -33,6 +33,9 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
[Inject]
private ILogger<Chat> Logger { get; init; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
private InnerScrolling scrollingArea = null!;
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom;
@ -192,7 +195,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.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger, this.RustService), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
// Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)

View File

@ -1,11 +1,11 @@
using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools;
using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
using RustService = AIStudio.Tools.RustService;
// ReSharper disable ClassNeverInstantiated.Global
@ -28,6 +28,9 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
[Inject]
private ILogger<Settings> Logger { get; init; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
private readonly List<ConfigurationSelectData<string>> availableProviders = new();
#region Overrides of ComponentBase
@ -114,7 +117,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
if (dialogResult is null || dialogResult.Canceled)
return;
var providerInstance = provider.CreateProvider(this.Logger);
var providerInstance = provider.CreateProvider(this.Logger, this.RustService);
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
if(deleteSecretResponse.Success)
{

View File

@ -1,7 +1,5 @@
using AIStudio;
using AIStudio.Agents;
using AIStudio.Settings;
using AIStudio.Tools;
using AIStudio.Tools.Services;
using Microsoft.Extensions.Logging.Console;
@ -13,113 +11,125 @@ using System.Reflection;
using Microsoft.Extensions.FileProviders;
#endif
if(args.Length == 0)
namespace AIStudio;
internal sealed class Program
{
Console.WriteLine("Please provide the port of the runtime API.");
return;
}
public static RustService RUST_SERVICE = null!;
public static Encryption ENCRYPTION = null!;
var rustApiPort = args[0];
using var rust = new Rust(rustApiPort);
var appPort = await rust.GetAppPort();
if(appPort == 0)
{
Console.WriteLine("Failed to get the app port from Rust.");
return;
}
// Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable:
var secretPasswordEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_PASSWORD");
if(string.IsNullOrWhiteSpace(secretPasswordEncoded))
{
Console.WriteLine("The AI_STUDIO_SECRET_PASSWORD environment variable is not set.");
return;
}
var secretPassword = Convert.FromBase64String(secretPasswordEncoded);
var secretKeySaltEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_KEY_SALT");
if(string.IsNullOrWhiteSpace(secretKeySaltEncoded))
{
Console.WriteLine("The AI_STUDIO_SECRET_KEY_SALT environment variable is not set.");
return;
}
var secretKeySalt = Convert.FromBase64String(secretKeySaltEncoded);
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;
config.SnackbarConfiguration.PreventDuplicates = false;
config.SnackbarConfiguration.NewestOnTop = false;
config.SnackbarConfiguration.ShowCloseIcon = true;
config.SnackbarConfiguration.VisibleStateDuration = 6_000; //milliseconds aka 6 seconds
config.SnackbarConfiguration.HideTransitionDuration = 500;
config.SnackbarConfiguration.ShowTransitionDuration = 500;
config.SnackbarConfiguration.SnackbarVariant = Variant.Outlined;
});
builder.Services.AddMudMarkdownServices();
builder.Services.AddSingleton(MessageBus.INSTANCE);
builder.Services.AddSingleton(rust);
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
builder.Services.AddSingleton<SettingsManager>();
builder.Services.AddSingleton<ThreadSafeRandom>();
builder.Services.AddTransient<HTMLParser>();
builder.Services.AddTransient<AgentTextContentCleaner>();
builder.Services.AddHostedService<UpdateService>();
builder.Services.AddHostedService<TemporaryChatService>();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddHubOptions(options =>
public static async Task Main(string[] args)
{
options.MaximumReceiveMessageSize = null;
options.ClientTimeoutInterval = TimeSpan.FromSeconds(1_200);
options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});
if(args.Length == 0)
{
Console.WriteLine("Please provide the port of the runtime API.");
return;
}
builder.Services.AddSingleton(new HttpClient
{
BaseAddress = new Uri($"http://localhost:{appPort}")
});
var rustApiPort = args[0];
using var rust = new RustService(rustApiPort);
var appPort = await rust.GetAppPort();
if(appPort == 0)
{
Console.WriteLine("Failed to get the app port from Rust.");
return;
}
builder.WebHost.UseUrls($"http://localhost:{appPort}");
// Read the secret key for the IPC from the AI_STUDIO_SECRET_KEY environment variable:
var secretPasswordEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_PASSWORD");
if(string.IsNullOrWhiteSpace(secretPasswordEncoded))
{
Console.WriteLine("The AI_STUDIO_SECRET_PASSWORD environment variable is not set.");
return;
}
var secretPassword = Convert.FromBase64String(secretPasswordEncoded);
var secretKeySaltEncoded = Environment.GetEnvironmentVariable("AI_STUDIO_SECRET_KEY_SALT");
if(string.IsNullOrWhiteSpace(secretKeySaltEncoded))
{
Console.WriteLine("The AI_STUDIO_SECRET_KEY_SALT environment variable is not set.");
return;
}
var secretKeySalt = Convert.FromBase64String(secretKeySaltEncoded);
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;
config.SnackbarConfiguration.PreventDuplicates = false;
config.SnackbarConfiguration.NewestOnTop = false;
config.SnackbarConfiguration.ShowCloseIcon = true;
config.SnackbarConfiguration.VisibleStateDuration = 6_000; //milliseconds aka 6 seconds
config.SnackbarConfiguration.HideTransitionDuration = 500;
config.SnackbarConfiguration.ShowTransitionDuration = 500;
config.SnackbarConfiguration.SnackbarVariant = Variant.Outlined;
});
builder.Services.AddMudMarkdownServices();
builder.Services.AddSingleton(MessageBus.INSTANCE);
builder.Services.AddSingleton(rust);
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
builder.Services.AddSingleton<SettingsManager>();
builder.Services.AddSingleton<ThreadSafeRandom>();
builder.Services.AddTransient<HTMLParser>();
builder.Services.AddTransient<AgentTextContentCleaner>();
builder.Services.AddHostedService<UpdateService>();
builder.Services.AddHostedService<TemporaryChatService>();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddHubOptions(options =>
{
options.MaximumReceiveMessageSize = null;
options.ClientTimeoutInterval = TimeSpan.FromSeconds(1_200);
options.HandshakeTimeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddSingleton(new HttpClient
{
BaseAddress = new Uri($"http://localhost:{appPort}")
});
builder.WebHost.UseUrls($"http://localhost:{appPort}");
#if DEBUG
builder.WebHost.UseWebRoot("wwwroot");
builder.WebHost.UseStaticWebAssets();
#endif
// Execute the builder to get the app:
var app = builder.Build();
// Initialize the encryption service:
var encryptionLogger = app.Services.GetRequiredService<ILogger<Encryption>>();
var encryption = new Encryption(encryptionLogger, secretPassword, secretKeySalt);
var encryptionInitializer = encryption.Initialize();
// Set the logger for the Rust service:
var rustLogger = app.Services.GetRequiredService<ILogger<RustService>>();
rust.SetLogger(rustLogger);
rust.SetEncryptor(encryption);
RUST_SERVICE = rust;
ENCRYPTION = encryption;
app.Use(Redirect.HandlerContentAsync);
#if DEBUG
builder.WebHost.UseWebRoot("wwwroot");
builder.WebHost.UseStaticWebAssets();
#endif
// Execute the builder to get the app:
var app = builder.Build();
// Initialize the encryption service:
var encryptionLogger = app.Services.GetRequiredService<ILogger<Encryption>>();
var encryption = new Encryption(encryptionLogger, secretPassword, secretKeySalt);
var encryptionInitializer = encryption.Initialize();
// Set the logger for the Rust service:
var rustLogger = app.Services.GetRequiredService<ILogger<Rust>>();
rust.SetLogger(rustLogger);
rust.SetEncryptor(encryption);
app.Use(Redirect.HandlerContentAsync);
#if DEBUG
app.UseStaticFiles();
app.UseDeveloperExceptionPage();
app.UseStaticFiles();
app.UseDeveloperExceptionPage();
#else
var fileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "wwwroot");
@ -131,12 +141,14 @@ app.UseStaticFiles(new StaticFileOptions
#endif
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
var serverTask = app.RunAsync();
var serverTask = app.RunAsync();
await encryptionInitializer;
await rust.AppIsReady();
await serverTask;
await encryptionInitializer;
await rust.AppIsReady();
await serverTask;
}
}

View File

@ -25,7 +25,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this);
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
@ -64,7 +64,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
var request = new HttpRequestMessage(HttpMethod.Post, "messages");
// Set the authorization header:
request.Headers.Add("x-api-key", requestedSecret.Secret);
request.Headers.Add("x-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the Anthropic version:
request.Headers.Add("anthropic-version", "2023-06-01");

View File

@ -1,3 +1,5 @@
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Provider;
/// <summary>
@ -15,6 +17,16 @@ public abstract class BaseProvider
/// </summary>
protected readonly ILogger logger;
static BaseProvider()
{
RUST_SERVICE = Program.RUST_SERVICE;
ENCRYPTION = Program.ENCRYPTION;
}
protected static readonly RustService RUST_SERVICE;
protected static readonly Encryption ENCRYPTION;
/// <summary>
/// Constructor for the base provider.
/// </summary>

View File

@ -27,7 +27,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this);
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
@ -73,7 +73,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// Set the authorization header:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", requestedSecret.Secret);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content:
request.Content = new StringContent(fireworksChatRequest, Encoding.UTF8, "application/json");

View File

@ -26,7 +26,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this);
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
@ -75,7 +75,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// Set the authorization header:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", requestedSecret.Secret);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content:
request.Content = new StringContent(mistralChatRequest, Encoding.UTF8, "application/json");
@ -153,9 +153,9 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
var secretKey = apiKeyProvisional switch
{
not null => apiKeyProvisional,
_ => await settings.GetAPIKey(jsRuntime, this) switch
_ => await RUST_SERVICE.GetAPIKey(this) switch
{
{ Success: true } result => result.Secret,
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
_ => null,
}
};

View File

@ -30,7 +30,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await settings.GetAPIKey(jsRuntime, this);
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
if(!requestedSecret.Success)
yield break;
@ -79,7 +79,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
// Set the authorization header:
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", requestedSecret.Secret);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
// Set the content:
request.Content = new StringContent(openAIChatRequest, Encoding.UTF8, "application/json");
@ -154,25 +154,25 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
/// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
{
return this.LoadModels(jsRuntime, settings, "gpt-", token, apiKeyProvisional);
return this.LoadModels("gpt-", token, apiKeyProvisional);
}
/// <inheritdoc />
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
{
return this.LoadModels(jsRuntime, settings, "dall-e-", token, apiKeyProvisional);
return this.LoadModels("dall-e-", token, apiKeyProvisional);
}
#endregion
private async Task<IEnumerable<Model>> LoadModels(IJSRuntime jsRuntime, SettingsManager settings, string prefix, CancellationToken token, string? apiKeyProvisional = null)
private async Task<IEnumerable<Model>> LoadModels(string prefix, CancellationToken token, string? apiKeyProvisional = null)
{
var secretKey = apiKeyProvisional switch
{
not null => apiKeyProvisional,
_ => await settings.GetAPIKey(jsRuntime, this) switch
_ => await RUST_SERVICE.GetAPIKey(this) switch
{
{ Success: true } result => result.Secret,
{ Success: true } result => await result.Secret.Decrypt(ENCRYPTION),
_ => null,
}
};

View File

@ -4,6 +4,8 @@ using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Provider;
public static class ProvidersExtensions
@ -33,8 +35,9 @@ public static class ProvidersExtensions
/// </summary>
/// <param name="providerSettings">The provider settings.</param>
/// <param name="logger">The logger to use.</param>
/// <param name="rustService">The Rust instance to use.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger)
public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger, RustService rustService)
{
try
{

View File

@ -5,7 +5,6 @@ using AIStudio.Assistants.RewriteImprove;
using AIStudio.Assistants.TextSummarizer;
using AIStudio.Assistants.EMail;
using AIStudio.Settings.DataModel;
using AIStudio.Tools;
using WritingStylesRewrite = AIStudio.Assistants.RewriteImprove.WritingStyles;
using WritingStylesEMail = AIStudio.Assistants.EMail.WritingStyles;

View File

@ -1,5 +1,4 @@
using AIStudio.Assistants.Agenda;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;

View File

@ -1,5 +1,4 @@
using AIStudio.Assistants.EMail;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;

View File

@ -1,5 +1,3 @@
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;
public sealed class DataGrammarSpelling

View File

@ -1,5 +1,4 @@
using AIStudio.Assistants.RewriteImprove;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;

View File

@ -1,5 +1,4 @@
using AIStudio.Assistants.TextSummarizer;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;

View File

@ -1,5 +1,3 @@
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel;
public sealed class DataTranslation

View File

@ -1,7 +1,6 @@
using AIStudio.Assistants.Coding;
using AIStudio.Assistants.IconFinder;
using AIStudio.Assistants.TextSummarizer;
using AIStudio.Tools;
namespace AIStudio.Settings.DataModel.PreviousModels;

View File

@ -5,6 +5,7 @@ using AIStudio.Provider;
using Microsoft.AspNetCore.Components;
using Host = AIStudio.Provider.SelfHosted.Host;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Settings;
@ -79,6 +80,12 @@ public partial class ProviderDialog : ComponentBase
[Inject]
private ILogger<ProviderDialog> Logger { get; init; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private Encryption Encryption { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary>
@ -136,7 +143,7 @@ public partial class ProviderDialog : ComponentBase
}
var loadedProviderSettings = this.CreateProviderSettings();
var provider = loadedProviderSettings.CreateProvider(this.Logger);
var provider = loadedProviderSettings.CreateProvider(this.Logger, this.RustService);
if(provider is NoProvider)
{
await base.OnInitializedAsync();
@ -144,10 +151,10 @@ public partial class ProviderDialog : ComponentBase
}
// Load the API key:
var requestedSecret = await this.SettingsManager.GetAPIKey(this.JsRuntime, provider);
var requestedSecret = await this.RustService.GetAPIKey(provider);
if(requestedSecret.Success)
{
this.dataAPIKey = requestedSecret.Secret;
this.dataAPIKey = await requestedSecret.Secret.Decrypt(this.Encryption);
// Now, we try to load the list of available models:
await this.ReloadModels();
@ -190,7 +197,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(this.Logger);
var provider = addedProviderSettings.CreateProvider(this.Logger, this.RustService);
// Store the API key in the OS secure storage:
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
@ -321,7 +328,7 @@ public partial class ProviderDialog : ComponentBase
private async Task ReloadModels()
{
var currentProviderSettings = this.CreateProviderSettings();
var provider = currentProviderSettings.CreateProvider(this.Logger);
var provider = currentProviderSettings.CreateProvider(this.Logger, this.RustService);
if(provider is NoProvider)
return;

View File

@ -42,24 +42,6 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
#region API Key Handling
private readonly record struct GetSecretRequest(string Destination, string UserName);
/// <summary>
/// Data structure for any requested secret.
/// </summary>
/// <param name="Success">True, when the secret was successfully retrieved.</param>
/// <param name="Secret">The secret, e.g., API key.</param>
/// <param name="Issue">The issue, when the secret could not be retrieved.</param>
public readonly record struct RequestedSecret(bool Success, string Secret, string Issue);
/// <summary>
/// Try to get the API key for the given provider.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="provider">The provider to get the API key for.</param>
/// <returns>The requested secret.</returns>
public async Task<RequestedSecret> GetAPIKey(IJSRuntime jsRuntime, IProvider provider) => await jsRuntime.InvokeAsync<RequestedSecret>("window.__TAURI__.invoke", "get_secret", new GetSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName));
private readonly record struct StoreSecretRequest(string Destination, string UserName, string Secret);
/// <summary>

View File

@ -93,7 +93,7 @@ public sealed class Encryption(ILogger<Encryption> logger, byte[] secretPassword
public async Task<string> Decrypt(EncryptedText encryptedData)
{
// Build a memory stream to access the given base64 encoded data:
await using var encodedEncryptedStream = new MemoryStream(Encoding.ASCII.GetBytes(encryptedData));
await using var encodedEncryptedStream = new MemoryStream(Encoding.ASCII.GetBytes(encryptedData.EncryptedData));
// Wrap around the base64 decoder stream:
await using var base64Stream = new CryptoStream(encodedEncryptedStream, new FromBase64Transform(), CryptoStreamMode.Read);
@ -102,11 +102,12 @@ public sealed class Encryption(ILogger<Encryption> logger, byte[] secretPassword
var readSaltBytes = new byte[16]; // 16 bytes = Guid
// Read the salt's bytes out of the stream:
var readBytes = await base64Stream.ReadAsync(readSaltBytes);
if(readBytes != 16)
var readBytes = 0;
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
while(readBytes < readSaltBytes.Length && !cts.Token.IsCancellationRequested)
{
logger.LogError($"Read {readBytes} bytes instead of 16 bytes for the salt.");
throw new CryptographicException("Failed to read the salt bytes.");
readBytes += await base64Stream.ReadAsync(readSaltBytes, readBytes, readSaltBytes.Length - readBytes, cts.Token);
await Task.Delay(TimeSpan.FromMilliseconds(60), cts.Token);
}
// Check the salt bytes:

View File

@ -0,0 +1,7 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tools.Rust;
public readonly record struct GetSecretRequest(
string Destination,
[property:JsonPropertyName("user_name")] string UserName);

View File

@ -0,0 +1,9 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for any requested secret.
/// </summary>
/// <param name="Success">True, when the secret was successfully retrieved.</param>
/// <param name="Secret">The secret, e.g., API key.</param>
/// <param name="Issue">The issue, when the secret could not be retrieved.</param>
public readonly record struct RequestedSecret(bool Success, EncryptedText Secret, string Issue);

View File

@ -1,4 +1,4 @@
namespace AIStudio.Tools;
namespace AIStudio.Tools.Rust;
/// <summary>
/// The response from the set clipboard operation.

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tools;
namespace AIStudio.Tools.Rust;
/// <summary>
/// The response of the update check.

View File

@ -1,19 +1,24 @@
using AIStudio.Provider;
using AIStudio.Tools.Rust;
// ReSharper disable NotAccessedPositionalProperty.Local
namespace AIStudio.Tools;
/// <summary>
/// Calling Rust functions.
/// </summary>
public sealed class Rust(string apiPort) : IDisposable
public sealed class RustService(string apiPort) : IDisposable
{
private readonly HttpClient http = new()
{
BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"),
};
private ILogger<Rust>? logger;
private ILogger<RustService>? logger;
private Encryption? encryptor;
public void SetLogger(ILogger<Rust> logService)
public void SetLogger(ILogger<RustService> logService)
{
this.logger = logService;
}
@ -87,7 +92,8 @@ public sealed class Rust(string apiPort) : IDisposable
var severity = Severity.Error;
try
{
var response = await this.http.PostAsync("/clipboard/set", new StringContent(await text.Encrypt(this.encryptor!)));
var encryptedText = await text.Encrypt(this.encryptor!);
var response = await this.http.PostAsync("/clipboard/set", new StringContent(encryptedText.EncryptedData));
if (!response.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to copy the text to the clipboard due to an network error: '{response.StatusCode}'");
@ -136,7 +142,7 @@ public sealed class Rust(string apiPort) : IDisposable
}
}
public async Task InstallUpdate(IJSRuntime jsRuntime)
public async Task InstallUpdate()
{
try
{
@ -150,6 +156,28 @@ public sealed class Rust(string apiPort) : IDisposable
}
}
/// <summary>
/// Try to get the API key for the given provider.
/// </summary>
/// <param name="provider">The provider to get the API key for.</param>
/// <returns>The requested secret.</returns>
public async Task<RequestedSecret> GetAPIKey(IProvider provider)
{
var secretRequest = new GetSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue.");
}
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>();
if (!secret.Success)
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}': '{secret.Issue}'");
return secret;
}
#region IDisposable
public void Dispose()

View File

@ -6,11 +6,11 @@ namespace AIStudio.Tools.Services;
/// Wire up the clipboard service to copy Markdown to the clipboard.
/// We use our own Rust-based clipboard service for this.
/// </summary>
public sealed class MarkdownClipboardService(Rust rust, ISnackbar snackbar) : IMudMarkdownClipboardService
public sealed class MarkdownClipboardService(RustService rust, ISnackbar snackbar) : IMudMarkdownClipboardService
{
private ISnackbar Snackbar { get; } = snackbar;
private Rust Rust { get; } = rust;
private RustService Rust { get; } = rust;
/// <summary>
/// Gets called when the user wants to copy the Markdown to the clipboard.

View File

@ -12,11 +12,11 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
private readonly SettingsManager settingsManager;
private readonly MessageBus messageBus;
private readonly Rust rust;
private readonly RustService rust;
private TimeSpan updateInterval;
public UpdateService(MessageBus messageBus, SettingsManager settingsManager, Rust rust)
public UpdateService(MessageBus messageBus, SettingsManager settingsManager, RustService rust)
{
this.settingsManager = settingsManager;
this.messageBus = messageBus;

View File

@ -1,7 +0,0 @@
namespace AIStudio.Tools;
/// <summary>
/// Model for setting clipboard text.
/// </summary>
/// <param name="Text">The text to set to the clipboard.</param>
public record SetClipboardText(string Text);

View File

@ -194,7 +194,7 @@ async fn main() {
//
tauri::async_runtime::spawn(async move {
_ = rocket::custom(figment)
.mount("/", routes![dotnet_port, dotnet_ready, set_clipboard, check_for_update, install_update])
.mount("/", routes![dotnet_port, dotnet_ready, set_clipboard, check_for_update, install_update, get_secret])
.ignite().await.unwrap()
.launch().await.unwrap();
});
@ -319,7 +319,7 @@ async fn main() {
})
.plugin(tauri_plugin_window_state::Builder::default().build())
.invoke_handler(tauri::generate_handler![
store_secret, get_secret, delete_secret
store_secret, delete_secret
])
.build(tauri::generate_context!())
.expect("Error while running Tauri application");
@ -776,36 +776,57 @@ struct StoreSecretResponse {
issue: String,
}
#[tauri::command]
fn get_secret(destination: String, user_name: String) -> RequestedSecret {
let service = format!("mindwork-ai-studio::{}", destination);
let entry = Entry::new(service.as_str(), user_name.as_str()).unwrap();
#[post("/secrets/get", data = "<request>")]
fn get_secret(request: Json<RequestSecret>) -> Json<RequestedSecret> {
let user_name = request.user_name.as_str();
let service = format!("mindwork-ai-studio::{}", request.destination);
let entry = Entry::new(service.as_str(), user_name).unwrap();
let secret = entry.get_password();
match secret {
Ok(s) => {
info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was retrieved successfully.");
RequestedSecret {
info!(Source = "Secret Store"; "Secret for '{service}' and user '{user_name}' was retrieved successfully.");
// Encrypt the secret:
let encrypted_secret = match ENCRYPTION.encrypt(s.as_str()) {
Ok(e) => e,
Err(e) => {
error!(Source = "Secret Store"; "Failed to encrypt the secret: {e}.");
return Json(RequestedSecret {
success: false,
secret: EncryptedText::new(String::from("")),
issue: format!("Failed to encrypt the secret: {e}"),
});
},
};
Json(RequestedSecret {
success: true,
secret: s,
secret: encrypted_secret,
issue: String::from(""),
}
})
},
Err(e) => {
error!(Source = "Secret Store"; "Failed to retrieve secret for {service} and user {user_name}: {e}.");
RequestedSecret {
error!(Source = "Secret Store"; "Failed to retrieve secret for '{service}' and user '{user_name}': {e}.");
Json(RequestedSecret {
success: false,
secret: String::from(""),
issue: e.to_string(),
}
secret: EncryptedText::new(String::from("")),
issue: format!("Failed to retrieve secret for '{service}' and user '{user_name}': {e}"),
})
},
}
}
#[derive(Deserialize)]
struct RequestSecret {
destination: String,
user_name: String,
}
#[derive(Serialize)]
struct RequestedSecret {
success: bool,
secret: String,
secret: EncryptedText,
issue: String,
}