Migrated the deletion of secrets from Tauri JS to the runtime API

This commit is contained in:
Thorsten Sommer 2024-08-29 11:28:11 +02:00
parent 54c2cd3740
commit 88ea640a68
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
25 changed files with 99 additions and 137 deletions

View File

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

View File

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

View File

@ -155,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.RustService), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
this.isProcessing = false;
this.StateHasChanged();

View File

@ -42,9 +42,6 @@ public partial class ContentBlockComponent : ComponentBase
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;

View File

@ -29,7 +29,7 @@ public sealed class ContentImage : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc />
public Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default)
public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default)
{
throw new NotImplementedException();
}

View File

@ -35,18 +35,18 @@ public sealed class ContentText : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc />
public async Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread? chatThread, CancellationToken token = default)
public async Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread? chatThread, CancellationToken token = default)
{
if(chatThread is null)
return;
// Store the last time we got a response. We use this later,
// Store the last time we got a response. We use this ater
// to determine whether we should notify the UI about the
// new content or not. Depends on the energy saving mode
// the user chose.
var last = DateTimeOffset.Now;
// Start another thread by using a task, to uncouple
// Start another thread by using a task to uncouple
// the UI thread from the AI processing:
await Task.Run(async () =>
{
@ -54,7 +54,7 @@ public sealed class ContentText : IContent
this.InitialRemoteWait = true;
// Iterate over the responses from the AI:
await foreach (var deltaText in provider.StreamChatCompletion(jsRuntime, settings, chatModel, chatThread, token))
await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, token))
{
// When the user cancels the request, we stop the loop:
if (token.IsCancellationRequested)
@ -89,7 +89,7 @@ public sealed class ContentText : IContent
}
// Stop the waiting animation (in case the loop
// was stopped or no content was received):
// was stopped, or no content was received):
this.InitialRemoteWait = false;
this.IsStreaming = false;
}, token);

View File

@ -42,5 +42,5 @@ public interface IContent
/// <summary>
/// Uses the provider to create the content.
/// </summary>
public Task CreateFromProviderAsync(IProvider provider, IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default);
public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default);
}

View File

@ -1,7 +1,5 @@
@using AIStudio.Components
@using AIStudio.Provider
@using AIStudio.Provider.SelfHosted
@using MudBlazor
<MudDialog>
<DialogContent>

View File

@ -75,9 +75,6 @@ public partial class ProviderDialog : ComponentBase
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private ILogger<ProviderDialog> Logger { get; init; } = null!;
@ -142,7 +139,7 @@ public partial class ProviderDialog : ComponentBase
}
var loadedProviderSettings = this.CreateProviderSettings();
var provider = loadedProviderSettings.CreateProvider(this.Logger, this.RustService);
var provider = loadedProviderSettings.CreateProvider(this.Logger);
if(provider is NoProvider)
{
await base.OnInitializedAsync();
@ -196,7 +193,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, this.RustService);
var provider = addedProviderSettings.CreateProvider(this.Logger);
// Store the API key in the OS secure storage:
var storeResponse = await this.RustService.SetAPIKey(provider, this.dataAPIKey);
@ -327,11 +324,11 @@ public partial class ProviderDialog : ComponentBase
private async Task ReloadModels()
{
var currentProviderSettings = this.CreateProviderSettings();
var provider = currentProviderSettings.CreateProvider(this.Logger, this.RustService);
var provider = currentProviderSettings.CreateProvider(this.Logger);
if(provider is NoProvider)
return;
var models = await provider.GetTextModels(this.JsRuntime, this.SettingsManager, this.dataAPIKey);
var models = await provider.GetTextModels(this.dataAPIKey);
// Order descending by ID means that the newest models probably come first:
var orderedModels = models.OrderByDescending(n => n.Id);

View File

@ -9,7 +9,6 @@ using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Pages;
@ -21,9 +20,6 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
public IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private ThreadSafeRandom RNG { get; init; } = null!;
@ -33,9 +29,6 @@ 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;
@ -195,7 +188,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.RustService), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
// Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)

View File

@ -19,9 +19,6 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
[Inject]
private IDialogService DialogService { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
[Inject]
private MessageBus MessageBus { get; init; } = null!;
@ -117,8 +114,8 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
if (dialogResult is null || dialogResult.Canceled)
return;
var providerInstance = provider.CreateProvider(this.Logger, this.RustService);
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
var providerInstance = provider.CreateProvider(this.Logger);
var deleteSecretResponse = await this.RustService.DeleteAPIKey(providerInstance);
if(deleteSecretResponse.Success)
{
this.SettingsManager.ConfigurationData.Providers.Remove(provider);

View File

@ -4,7 +4,6 @@ using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
namespace AIStudio.Provider.Anthropic;
@ -22,7 +21,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
public string InstanceName { get; set; } = "Anthropic";
/// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@ -137,14 +136,14 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(new[]
{
@ -157,7 +156,7 @@ public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://ap
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Model>());
}

View File

@ -4,7 +4,6 @@ using System.Text;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider.Fireworks;
@ -24,7 +23,7 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
public string InstanceName { get; set; } = "Fireworks.ai";
/// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@ -139,20 +138,20 @@ public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.firew
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Model>());
}
/// <inheritdoc />
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Model>());
}

View File

@ -1,5 +1,4 @@
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider;
@ -22,44 +21,36 @@ public interface IProvider
/// <summary>
/// Starts a chat completion stream.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="settings">The settings manager to access the API key.</param>
/// <param name="chatModel">The model to use for chat completion.</param>
/// <param name="chatThread">The chat thread to continue.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The chat completion stream.</returns>
public IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, CancellationToken token = default);
public IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, CancellationToken token = default);
/// <summary>
/// Starts an image completion stream.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="settings">The settings manager to access the API key.</param>
/// <param name="imageModel">The model to use for image completion.</param>
/// <param name="promptPositive">The positive prompt.</param>
/// <param name="promptNegative">The negative prompt.</param>
/// <param name="referenceImageURL">The reference image URL.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The image completion stream.</returns>
public IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, CancellationToken token = default);
public IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, CancellationToken token = default);
/// <summary>
/// Load all possible text models that can be used with this provider.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="settings">The settings manager to access the API key.</param>
/// <param name="apiKeyProvisional">The provisional API key to use. Useful when the user is adding a new provider. When null, the stored API key is used.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The list of text models.</returns>
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default);
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default);
/// <summary>
/// Load all possible image models that can be used with this provider.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="settings">The settings manager to access the API key.</param>
/// <param name="apiKeyProvisional">The provisional API key to use. Useful when the user is adding a new provider. When null, the stored API key is used.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The list of image models.</returns>
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default);
public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default);
}

View File

@ -5,7 +5,6 @@ using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
namespace AIStudio.Provider.Mistral;
@ -23,7 +22,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
public string InstanceName { get; set; } = "Mistral";
/// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@ -141,14 +140,14 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public async Task<IEnumerable<Provider.Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
var secretKey = apiKeyProvisional switch
{
@ -179,7 +178,7 @@ public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Provider.Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Provider.Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Provider.Model>());
}

View File

@ -1,7 +1,6 @@
using System.Runtime.CompilerServices;
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider;
@ -13,17 +12,17 @@ public class NoProvider : IProvider
public string InstanceName { get; set; } = "None";
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) => Task.FromResult<IEnumerable<Model>>([]);
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatChatThread, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatChatThread, [EnumeratorCancellation] CancellationToken token = default)
{
await Task.FromResult(0);
yield break;
}
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
await Task.FromResult(0);
yield break;

View File

@ -4,7 +4,6 @@ using System.Text;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Settings;
namespace AIStudio.Provider.OpenAI;
@ -27,7 +26,7 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
public string InstanceName { get; set; } = "OpenAI";
/// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<string> StreamChatCompletion(Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Get the API key:
var requestedSecret = await RUST_SERVICE.GetAPIKey(this);
@ -145,20 +144,20 @@ public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.o
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return this.LoadModels("gpt-", token, apiKeyProvisional);
}
/// <inheritdoc />
public Task<IEnumerable<Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return this.LoadModels("dall-e-", token, apiKeyProvisional);
}

View File

@ -4,8 +4,6 @@ using AIStudio.Provider.Mistral;
using AIStudio.Provider.OpenAI;
using AIStudio.Provider.SelfHosted;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Provider;
public static class ProvidersExtensions
@ -35,9 +33,8 @@ 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, RustService rustService)
public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger)
{
try
{

View File

@ -4,7 +4,6 @@ using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Provider.OpenAI;
using AIStudio.Settings;
namespace AIStudio.Provider.SelfHosted;
@ -21,7 +20,8 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
public string InstanceName { get; set; } = "Self-hosted";
public async IAsyncEnumerable<string> StreamChatCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
/// <inheritdoc />
public async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default)
{
// Prepare the system prompt:
var systemPrompt = new Message
@ -129,14 +129,14 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(IJSRuntime jsRuntime, SettingsManager settings, Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
public async IAsyncEnumerable<ImageURL> StreamImageCompletion(Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
{
yield break;
}
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
public async Task<IEnumerable<Provider.Model>> GetTextModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
try
{
@ -169,7 +169,7 @@ public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provide
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
/// <inheritdoc />
public Task<IEnumerable<Provider.Model>> GetImageModels(IJSRuntime jsRuntime, SettingsManager settings, string? apiKeyProvisional = null, CancellationToken token = default)
public Task<IEnumerable<Provider.Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
{
return Task.FromResult(Enumerable.Empty<Provider.Model>());
}

View File

@ -1,7 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using AIStudio.Provider;
using AIStudio.Settings.DataModel;
// ReSharper disable NotAccessedPositionalProperty.Local
@ -40,28 +39,6 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
private bool IsSetUp => !string.IsNullOrWhiteSpace(ConfigDirectory) && !string.IsNullOrWhiteSpace(DataDirectory);
#region API Key Handling
private readonly record struct DeleteSecretRequest(string Destination, string UserName);
/// <summary>
/// Data structure for deleting a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully deleted or not found.</param>
/// <param name="Issue">The issue, when the secret could not be deleted.</param>
/// <param name="WasEntryFound">True, when the entry was found and deleted.</param>
public readonly record struct DeleteSecretResponse(bool Success, string Issue, bool WasEntryFound);
/// <summary>
/// Tries to delete 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 delete the API key for.</param>
/// <returns>The delete secret response.</returns>
public async Task<DeleteSecretResponse> DeleteAPIKey(IJSRuntime jsRuntime, IProvider provider) => await jsRuntime.InvokeAsync<DeleteSecretResponse>("window.__TAURI__.invoke", "delete_secret", new DeleteSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName));
#endregion
/// <summary>
/// Loads the settings from the file system.
/// </summary>

View File

@ -0,0 +1,9 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for deleting a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully deleted or not found.</param>
/// <param name="Issue">The issue, when the secret could not be deleted.</param>
/// <param name="WasEntryFound">True, when the entry was found and deleted.</param>
public readonly record struct DeleteSecretResponse(bool Success, string Issue, bool WasEntryFound);

View File

@ -1,6 +0,0 @@
namespace AIStudio.Tools.Rust;
public readonly record struct GetSecretRequest(
string Destination,
string UserName
);

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools.Rust;
public readonly record struct SelectSecretRequest(string Destination, string UserName);

View File

@ -170,7 +170,7 @@ public sealed class RustService(string apiPort) : IDisposable
/// <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 secretRequest = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
@ -209,6 +209,27 @@ public sealed class RustService(string apiPort) : IDisposable
return state;
}
/// <summary>
/// Tries to delete the API key for the given provider.
/// </summary>
/// <param name="provider">The provider to delete the API key for.</param>
/// <returns>The delete secret response.</returns>
public async Task<DeleteSecretResponse> DeleteAPIKey(IProvider provider)
{
var request = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to delete the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the API key due to an API issue."};
}
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>();
if (!state.Success)
this.logger!.LogError($"Failed to delete the API key for provider '{provider.Id}': '{state.Issue}'");
return state;
}
#region IDisposable

View File

@ -194,7 +194,10 @@ 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, get_secret, store_secret])
.mount("/", routes![
dotnet_port, dotnet_ready, set_clipboard, check_for_update, install_update,
get_secret, store_secret, delete_secret
])
.ignite().await.unwrap()
.launch().await.unwrap();
});
@ -318,9 +321,6 @@ async fn main() {
Ok(())
})
.plugin(tauri_plugin_window_state::Builder::default().build())
.invoke_handler(tauri::generate_handler![
delete_secret
])
.build(tauri::generate_context!())
.expect("Error while running Tauri application");
@ -849,38 +849,39 @@ struct RequestedSecret {
issue: String,
}
#[tauri::command]
fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse {
let service = format!("mindwork-ai-studio::{}", destination);
let entry = Entry::new(service.as_str(), user_name.as_str()).unwrap();
#[post("/secrets/delete", data = "<request>")]
fn delete_secret(request: Json<RequestSecret>) -> Json<DeleteSecretResponse> {
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 result = entry.delete_credential();
match result {
Ok(_) => {
warn!(Source = "Secret Store"; "Secret for {service} and user {user_name} was deleted successfully.");
DeleteSecretResponse {
Json(DeleteSecretResponse {
success: true,
was_entry_found: true,
issue: String::from(""),
}
})
},
Err(NoEntry) => {
warn!(Source = "Secret Store"; "No secret for {service} and user {user_name} was found.");
DeleteSecretResponse {
Json(DeleteSecretResponse {
success: true,
was_entry_found: false,
issue: String::from(""),
}
})
}
Err(e) => {
error!(Source = "Secret Store"; "Failed to delete secret for {service} and user {user_name}: {e}.");
DeleteSecretResponse {
Json(DeleteSecretResponse {
success: false,
was_entry_found: false,
issue: e.to_string(),
}
})
},
}
}