mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-02-05 17:49:05 +00:00
Implement support for self-hosted and local LLMs (#20)
This commit is contained in:
parent
6cc1d37db8
commit
29263660fc
@ -13,6 +13,7 @@ public partial class Changelog
|
|||||||
|
|
||||||
public static readonly Log[] LOGS =
|
public static readonly Log[] LOGS =
|
||||||
[
|
[
|
||||||
|
new (159, "v0.6.3, build 159 (2024-07-03 18:26 UTC)", "v0.6.3.md"),
|
||||||
new (158, "v0.6.2, build 158 (2024-07-01 18:03 UTC)", "v0.6.2.md"),
|
new (158, "v0.6.2, build 158 (2024-07-01 18:03 UTC)", "v0.6.2.md"),
|
||||||
new (157, "v0.6.1, build 157 (2024-06-30 19:00 UTC)", "v0.6.1.md"),
|
new (157, "v0.6.1, build 157 (2024-06-30 19:00 UTC)", "v0.6.1.md"),
|
||||||
new (156, "v0.6.0, build 156 (2024-06-30 12:49 UTC)", "v0.6.0.md"),
|
new (156, "v0.6.0, build 156 (2024-06-30 12:49 UTC)", "v0.6.0.md"),
|
||||||
|
@ -101,7 +101,7 @@ public partial class Chat : ComponentBase
|
|||||||
// Use the selected provider to get the AI response.
|
// Use the selected provider to get the AI response.
|
||||||
// By awaiting this line, we wait for the entire
|
// By awaiting this line, we wait for the entire
|
||||||
// content to be streamed.
|
// content to be streamed.
|
||||||
await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread);
|
await aiText.CreateFromProviderAsync(this.selectedProvider.UsedProvider.CreateProvider(this.selectedProvider.InstanceName, this.selectedProvider.Hostname), this.JsRuntime, this.SettingsManager, this.selectedProvider.Model, this.chatThread);
|
||||||
|
|
||||||
// Disable the stream state:
|
// Disable the stream state:
|
||||||
this.isStreaming = false;
|
this.isStreaming = false;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@page "/settings"
|
@page "/settings"
|
||||||
|
@using AIStudio.Provider
|
||||||
|
|
||||||
<MudText Typo="Typo.h3" Class="mb-12">Settings</MudText>
|
<MudText Typo="Typo.h3" Class="mb-12">Settings</MudText>
|
||||||
|
|
||||||
@ -11,7 +12,7 @@
|
|||||||
<col style="width: 12em;"/>
|
<col style="width: 12em;"/>
|
||||||
<col style="width: 12em;"/>
|
<col style="width: 12em;"/>
|
||||||
<col/>
|
<col/>
|
||||||
<col style="width: 20em;"/>
|
<col style="width: 34em;"/>
|
||||||
</ColGroup>
|
</ColGroup>
|
||||||
<HeaderContent>
|
<HeaderContent>
|
||||||
<MudTh>#</MudTh>
|
<MudTh>#</MudTh>
|
||||||
@ -24,12 +25,20 @@
|
|||||||
<MudTd>@context.Num</MudTd>
|
<MudTd>@context.Num</MudTd>
|
||||||
<MudTd>@context.InstanceName</MudTd>
|
<MudTd>@context.InstanceName</MudTd>
|
||||||
<MudTd>@context.UsedProvider</MudTd>
|
<MudTd>@context.UsedProvider</MudTd>
|
||||||
<MudTd>@context.Model</MudTd>
|
<MudTd>
|
||||||
|
@if(context.UsedProvider is not Providers.SELF_HOSTED)
|
||||||
|
@context.Model
|
||||||
|
else
|
||||||
|
@("as selected by provider")
|
||||||
|
</MudTd>
|
||||||
<MudTd Style="text-align: left;">
|
<MudTd Style="text-align: left;">
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="mr-2" OnClick="() => this.EditProvider(context)">
|
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.OpenInBrowser" Class="ma-2" Href="@this.GetProviderDashboardURL(context.UsedProvider)" Target="_blank" Disabled="@(context.UsedProvider is Providers.NONE or Providers.SELF_HOSTED)">
|
||||||
|
Open Dashboard
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditProvider(context)">
|
||||||
Edit
|
Edit
|
||||||
</MudButton>
|
</MudButton>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="mr-2" OnClick="() => this.DeleteProvider(context)">
|
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteProvider(context)">
|
||||||
Delete
|
Delete
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
|
@ -50,6 +50,8 @@ public partial class Settings : ComponentBase
|
|||||||
{ x => x.DataInstanceName, provider.InstanceName },
|
{ x => x.DataInstanceName, provider.InstanceName },
|
||||||
{ x => x.DataProvider, provider.UsedProvider },
|
{ x => x.DataProvider, provider.UsedProvider },
|
||||||
{ x => x.DataModel, provider.Model },
|
{ x => x.DataModel, provider.Model },
|
||||||
|
{ x => x.DataHostname, provider.Hostname },
|
||||||
|
{ x => x.IsSelfHosted, provider.IsSelfHosted },
|
||||||
{ x => x.IsEditing, true },
|
{ x => x.IsEditing, true },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,7 +83,7 @@ public partial class Settings : ComponentBase
|
|||||||
if (dialogResult.Canceled)
|
if (dialogResult.Canceled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var providerInstance = provider.UsedProvider.CreateProvider(provider.InstanceName);
|
var providerInstance = provider.UsedProvider.CreateProvider(provider.InstanceName, provider.Hostname);
|
||||||
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
|
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
|
||||||
if(deleteSecretResponse.Success)
|
if(deleteSecretResponse.Success)
|
||||||
{
|
{
|
||||||
@ -90,5 +92,14 @@ public partial class Settings : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetProviderDashboardURL(Providers provider) => provider switch
|
||||||
|
{
|
||||||
|
Providers.OPEN_AI => "https://platform.openai.com/usage",
|
||||||
|
Providers.MISTRAL => "https://console.mistral.ai/usage/",
|
||||||
|
Providers.ANTHROPIC => "https://console.anthropic.com/settings/plans",
|
||||||
|
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
using AIStudio.Provider.Anthropic;
|
using AIStudio.Provider.Anthropic;
|
||||||
using AIStudio.Provider.Mistral;
|
using AIStudio.Provider.Mistral;
|
||||||
using AIStudio.Provider.OpenAI;
|
using AIStudio.Provider.OpenAI;
|
||||||
|
using AIStudio.Provider.SelfHosted;
|
||||||
|
|
||||||
namespace AIStudio.Provider;
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
@ -10,9 +11,12 @@ namespace AIStudio.Provider;
|
|||||||
public enum Providers
|
public enum Providers
|
||||||
{
|
{
|
||||||
NONE,
|
NONE,
|
||||||
|
|
||||||
OPEN_AI,
|
OPEN_AI,
|
||||||
ANTHROPIC,
|
ANTHROPIC,
|
||||||
MISTRAL,
|
MISTRAL,
|
||||||
|
|
||||||
|
SELF_HOSTED,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -27,11 +31,14 @@ public static class ExtensionsProvider
|
|||||||
/// <returns>The human-readable name of the provider.</returns>
|
/// <returns>The human-readable name of the provider.</returns>
|
||||||
public static string ToName(this Providers provider) => provider switch
|
public static string ToName(this Providers provider) => provider switch
|
||||||
{
|
{
|
||||||
|
Providers.NONE => "No provider selected",
|
||||||
|
|
||||||
Providers.OPEN_AI => "OpenAI",
|
Providers.OPEN_AI => "OpenAI",
|
||||||
Providers.ANTHROPIC => "Anthropic",
|
Providers.ANTHROPIC => "Anthropic",
|
||||||
Providers.MISTRAL => "Mistral",
|
Providers.MISTRAL => "Mistral",
|
||||||
|
|
||||||
Providers.NONE => "No provider selected",
|
Providers.SELF_HOSTED => "Self-hosted",
|
||||||
|
|
||||||
_ => "Unknown",
|
_ => "Unknown",
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,13 +47,16 @@ public static class ExtensionsProvider
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="provider">The provider value.</param>
|
/// <param name="provider">The provider value.</param>
|
||||||
/// <param name="instanceName">The used instance name.</param>
|
/// <param name="instanceName">The used instance name.</param>
|
||||||
|
/// <param name="hostname">The hostname of the provider.</param>
|
||||||
/// <returns>The provider instance.</returns>
|
/// <returns>The provider instance.</returns>
|
||||||
public static IProvider CreateProvider(this Providers provider, string instanceName) => provider switch
|
public static IProvider CreateProvider(this Providers provider, string instanceName, string hostname = "http://localhost:1234") => provider switch
|
||||||
{
|
{
|
||||||
Providers.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName },
|
Providers.OPEN_AI => new ProviderOpenAI { InstanceName = instanceName },
|
||||||
Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName },
|
Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = instanceName },
|
||||||
Providers.MISTRAL => new ProviderMistral { InstanceName = instanceName },
|
Providers.MISTRAL => new ProviderMistral { InstanceName = instanceName },
|
||||||
|
|
||||||
|
Providers.SELF_HOSTED => new ProviderSelfHosted(hostname) { InstanceName = instanceName },
|
||||||
|
|
||||||
_ => new NoProvider(),
|
_ => new NoProvider(),
|
||||||
};
|
};
|
||||||
}
|
}
|
16
app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs
Normal file
16
app/MindWork AI Studio/Provider/SelfHosted/ChatRequest.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
namespace AIStudio.Provider.SelfHosted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The chat request model.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Model">Which model to use for chat completion.</param>
|
||||||
|
/// <param name="Messages">The chat messages.</param>
|
||||||
|
/// <param name="Stream">Whether to stream the chat completion.</param>
|
||||||
|
/// <param name="MaxTokens">The maximum number of tokens to generate.</param>
|
||||||
|
public readonly record struct ChatRequest(
|
||||||
|
string Model,
|
||||||
|
IList<Message> Messages,
|
||||||
|
bool Stream,
|
||||||
|
|
||||||
|
int MaxTokens
|
||||||
|
);
|
8
app/MindWork AI Studio/Provider/SelfHosted/Message.cs
Normal file
8
app/MindWork AI Studio/Provider/SelfHosted/Message.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace AIStudio.Provider.SelfHosted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chat message model.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Content">The text content of the message.</param>
|
||||||
|
/// <param name="Role">The role of the message.</param>
|
||||||
|
public readonly record struct Message(string Content, string Role);
|
@ -0,0 +1,5 @@
|
|||||||
|
namespace AIStudio.Provider.SelfHosted;
|
||||||
|
|
||||||
|
public readonly record struct ModelsResponse(string Object, Model[] Data);
|
||||||
|
|
||||||
|
public readonly record struct Model(string Id, string Object, string OwnedBy);
|
162
app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs
Normal file
162
app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using AIStudio.Chat;
|
||||||
|
using AIStudio.Provider.OpenAI;
|
||||||
|
using AIStudio.Settings;
|
||||||
|
|
||||||
|
namespace AIStudio.Provider.SelfHosted;
|
||||||
|
|
||||||
|
public sealed class ProviderSelfHosted(string hostname) : BaseProvider($"{hostname}/v1/"), IProvider
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||||
|
};
|
||||||
|
|
||||||
|
#region Implementation of IProvider
|
||||||
|
|
||||||
|
public string Id => "Self-hosted";
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
// Prepare the system prompt:
|
||||||
|
var systemPrompt = new Message
|
||||||
|
{
|
||||||
|
Role = "system",
|
||||||
|
Content = chatThread.SystemPrompt,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare the OpenAI HTTP chat request:
|
||||||
|
var providerChatRequest = JsonSerializer.Serialize(new ChatRequest
|
||||||
|
{
|
||||||
|
Model = (await this.GetTextModels(jsRuntime, settings, token: token)).First().Id,
|
||||||
|
|
||||||
|
// Build the messages:
|
||||||
|
// - First of all the system prompt
|
||||||
|
// - Then none-empty user and AI messages
|
||||||
|
Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message
|
||||||
|
{
|
||||||
|
Role = n.Role switch
|
||||||
|
{
|
||||||
|
ChatRole.USER => "user",
|
||||||
|
ChatRole.AI => "assistant",
|
||||||
|
ChatRole.SYSTEM => "system",
|
||||||
|
|
||||||
|
_ => "user",
|
||||||
|
},
|
||||||
|
|
||||||
|
Content = n.Content switch
|
||||||
|
{
|
||||||
|
ContentText text => text.Text,
|
||||||
|
_ => string.Empty,
|
||||||
|
}
|
||||||
|
}).ToList()],
|
||||||
|
|
||||||
|
// Right now, we only support streaming completions:
|
||||||
|
Stream = true,
|
||||||
|
MaxTokens = -1,
|
||||||
|
}, JSON_SERIALIZER_OPTIONS);
|
||||||
|
|
||||||
|
// Build the HTTP post request:
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, "chat/completions");
|
||||||
|
|
||||||
|
// Set the content:
|
||||||
|
request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
// Send the request with the ResponseHeadersRead option.
|
||||||
|
// This allows us to read the stream as soon as the headers are received.
|
||||||
|
// This is important because we want to stream the responses.
|
||||||
|
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||||
|
|
||||||
|
// Open the response stream:
|
||||||
|
var providerStream = await response.Content.ReadAsStreamAsync(token);
|
||||||
|
|
||||||
|
// Add a stream reader to read the stream, line by line:
|
||||||
|
var streamReader = new StreamReader(providerStream);
|
||||||
|
|
||||||
|
// Read the stream, line by line:
|
||||||
|
while(!streamReader.EndOfStream)
|
||||||
|
{
|
||||||
|
// Check if the token is cancelled:
|
||||||
|
if(token.IsCancellationRequested)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
// Read the next line:
|
||||||
|
var line = await streamReader.ReadLineAsync(token);
|
||||||
|
|
||||||
|
// Skip empty lines:
|
||||||
|
if(string.IsNullOrWhiteSpace(line))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Skip lines that do not start with "data: ". Regard
|
||||||
|
// to the specification, we only want to read the data lines:
|
||||||
|
if(!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check if the line is the end of the stream:
|
||||||
|
if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture))
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
ResponseStreamLine providerResponse;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// We know that the line starts with "data: ". Hence, we can
|
||||||
|
// skip the first 6 characters to get the JSON data after that.
|
||||||
|
var jsonData = line[6..];
|
||||||
|
|
||||||
|
// Deserialize the JSON data:
|
||||||
|
providerResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Skip invalid JSON data:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip empty responses:
|
||||||
|
if(providerResponse == default || providerResponse.Choices.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Yield the response:
|
||||||
|
yield return providerResponse.Choices[0].Delta.Content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
|
var response = await this.httpClient.SendAsync(request, token);
|
||||||
|
if(!response.IsSuccessStatusCode)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||||
|
if (modelResponse.Data.Length > 1)
|
||||||
|
Console.WriteLine("Warning: multiple models found; using the first one.");
|
||||||
|
|
||||||
|
var firstModel = modelResponse.Data.First();
|
||||||
|
return [ new Provider.Model(firstModel.Id) ];
|
||||||
|
}
|
||||||
|
|
||||||
|
#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)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Enumerable.Empty<Provider.Model>());
|
||||||
|
}
|
||||||
|
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
@ -9,7 +9,7 @@ public sealed class Data
|
|||||||
/// The version of the settings file. Allows us to upgrade the settings
|
/// The version of the settings file. Allows us to upgrade the settings
|
||||||
/// when a new version is available.
|
/// when a new version is available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Version Version { get; init; } = Version.V1;
|
public Version Version { get; init; } = Version.V2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of configured providers.
|
/// List of configured providers.
|
||||||
|
@ -9,8 +9,10 @@ namespace AIStudio.Settings;
|
|||||||
/// <param name="Id">The provider's ID.</param>
|
/// <param name="Id">The provider's ID.</param>
|
||||||
/// <param name="InstanceName">The provider's instance name. Useful for multiple instances of the same provider, e.g., to distinguish between different OpenAI API keys.</param>
|
/// <param name="InstanceName">The provider's instance name. Useful for multiple instances of the same provider, e.g., to distinguish between different OpenAI API keys.</param>
|
||||||
/// <param name="UsedProvider">The provider used.</param>
|
/// <param name="UsedProvider">The provider used.</param>
|
||||||
|
/// <param name="IsSelfHosted">Whether the provider is self-hosted.</param>
|
||||||
|
/// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param>
|
||||||
/// <param name="Model">The LLM model to use for chat.</param>
|
/// <param name="Model">The LLM model to use for chat.</param>
|
||||||
public readonly record struct Provider(uint Num, string Id, string InstanceName, Providers UsedProvider, Model Model)
|
public readonly record struct Provider(uint Num, string Id, string InstanceName, Providers UsedProvider, Model Model, bool IsSelfHosted = false, string Hostname = "http://localhost:1234")
|
||||||
{
|
{
|
||||||
#region Overrides of ValueType
|
#region Overrides of ValueType
|
||||||
|
|
||||||
@ -21,6 +23,9 @@ public readonly record struct Provider(uint Num, string Id, string InstanceName,
|
|||||||
/// <returns>A string that represents the current provider in a human-readable format.</returns>
|
/// <returns>A string that represents the current provider in a human-readable format.</returns>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
if(this.IsSelfHosted)
|
||||||
|
return $"{this.InstanceName} ({this.UsedProvider.ToName()}, {this.Hostname}, {this.Model})";
|
||||||
|
|
||||||
return $"{this.InstanceName} ({this.UsedProvider.ToName()}, {this.Model})";
|
return $"{this.InstanceName} ({this.UsedProvider.ToName()}, {this.Model})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,53 @@
|
|||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||||
|
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||||
|
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||||
|
<MudSelect @bind-Value="@this.DataProvider" Label="Provider" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.ValidatingProvider">
|
||||||
|
@foreach (Providers provider in Enum.GetValues(typeof(Providers)))
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@provider">@provider</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
<MudButton Disabled="@this.IsSelfHostedOrNone" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.OpenInBrowser" Href="@this.GetProviderCreationURL()" Target="_blank">Create account</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||||
|
<MudTextField
|
||||||
|
T="string"
|
||||||
|
@bind-Text="@this.dataAPIKey"
|
||||||
|
Label="API Key"
|
||||||
|
Disabled="@this.IsSelfHostedOrNone"
|
||||||
|
Class="mb-3"
|
||||||
|
Adornment="Adornment.Start"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.VpnKey"
|
||||||
|
AdornmentColor="Color.Info"
|
||||||
|
InputType="InputType.Password"
|
||||||
|
Validation="@this.ValidatingAPIKey"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MudTextField
|
||||||
|
T="string"
|
||||||
|
@bind-Text="@this.DataHostname"
|
||||||
|
Label="Hostname"
|
||||||
|
Disabled="@this.IsCloudProvider"
|
||||||
|
Class="mb-3"
|
||||||
|
Adornment="Adornment.Start"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.Dns"
|
||||||
|
AdornmentColor="Color.Info"
|
||||||
|
Validation="@this.ValidatingHostname"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||||
|
<MudButton Disabled="@(!this.CanLoadModels)" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="this.ReloadModels">Load</MudButton>
|
||||||
|
<MudSelect Disabled="@this.IsSelfHostedOrNone" @bind-Value="@this.DataModel" Label="Model" Class="mb-3" OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.ValidatingModel">
|
||||||
|
@foreach (var model in this.availableModels)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@model">@model</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||||
<MudTextField
|
<MudTextField
|
||||||
T="string"
|
T="string"
|
||||||
@ -17,37 +64,6 @@
|
|||||||
UserAttributes="@INSTANCE_NAME_ATTRIBUTES"
|
UserAttributes="@INSTANCE_NAME_ATTRIBUTES"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
|
||||||
<MudSelect @bind-Value="@this.DataProvider" Label="Provider" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.ValidatingProvider">
|
|
||||||
@foreach (Providers provider in Enum.GetValues(typeof(Providers)))
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@provider">@provider</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
|
|
||||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
|
||||||
<MudTextField
|
|
||||||
T="string"
|
|
||||||
@bind-Text="@this.dataAPIKey"
|
|
||||||
Label="API Key"
|
|
||||||
Class="mb-3"
|
|
||||||
Adornment="Adornment.Start"
|
|
||||||
AdornmentIcon="@Icons.Material.Filled.VpnKey"
|
|
||||||
AdornmentColor="Color.Info"
|
|
||||||
InputType="InputType.Password"
|
|
||||||
Validation="@this.ValidatingAPIKey"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
|
||||||
<MudButton Disabled="@(!this.CanLoadModels)" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Refresh" OnClick="this.ReloadModels">Reload</MudButton>
|
|
||||||
<MudSelect @bind-Value="@this.DataModel" Label="Model" Class="mb-3" OpenIcon="@Icons.Material.Filled.FaceRetouchingNatural" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.ValidatingModel">
|
|
||||||
@foreach (var model in this.availableModels)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="@model">@model</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
</MudForm>
|
</MudForm>
|
||||||
|
|
||||||
@if (this.dataIssues.Any())
|
@if (this.dataIssues.Any())
|
||||||
|
@ -32,6 +32,18 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public string DataInstanceName { get; set; } = string.Empty;
|
public string DataInstanceName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The chosen hostname for self-hosted providers.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public string DataHostname { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this provider self-hosted?
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public bool IsSelfHosted { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The provider to use.
|
/// The provider to use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -99,6 +111,7 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
this.dataAPIKey = requestedSecret.Secret;
|
this.dataAPIKey = requestedSecret.Secret;
|
||||||
|
|
||||||
// Now, we try to load the list of available models:
|
// Now, we try to load the list of available models:
|
||||||
|
if(this.DataProvider is not Providers.SELF_HOSTED)
|
||||||
await this.ReloadModels();
|
await this.ReloadModels();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -142,6 +155,8 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
InstanceName = this.DataInstanceName,
|
InstanceName = this.DataInstanceName,
|
||||||
UsedProvider = this.DataProvider,
|
UsedProvider = this.DataProvider,
|
||||||
Model = this.DataModel,
|
Model = this.DataModel,
|
||||||
|
IsSelfHosted = this.DataProvider is Providers.SELF_HOSTED,
|
||||||
|
Hostname = this.DataHostname,
|
||||||
};
|
};
|
||||||
|
|
||||||
// We need to instantiate the provider to store the API key:
|
// We need to instantiate the provider to store the API key:
|
||||||
@ -169,33 +184,49 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
|
|
||||||
private string? ValidatingModel(Model model)
|
private string? ValidatingModel(Model model)
|
||||||
{
|
{
|
||||||
|
if(this.DataProvider is Providers.SELF_HOSTED)
|
||||||
|
return null;
|
||||||
|
|
||||||
if (model == default)
|
if (model == default)
|
||||||
return "Please select a model.";
|
return "Please select a model.";
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex("^[a-zA-Z0-9 ]+$")]
|
[GeneratedRegex(@"^[a-zA-Z0-9\-_. ]+$")]
|
||||||
private static partial Regex InstanceNameRegex();
|
private static partial Regex InstanceNameRegex();
|
||||||
|
|
||||||
|
private static readonly string[] RESERVED_NAMES = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" };
|
||||||
|
|
||||||
private string? ValidatingInstanceName(string instanceName)
|
private string? ValidatingInstanceName(string instanceName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(instanceName))
|
if (string.IsNullOrWhiteSpace(instanceName))
|
||||||
return "Please enter an instance name.";
|
return "Please enter an instance name.";
|
||||||
|
|
||||||
if(instanceName.StartsWith(' '))
|
if (instanceName.StartsWith(' ') || instanceName.StartsWith('.'))
|
||||||
return "The instance name must not start with a space.";
|
return "The instance name must not start with a space or a dot.";
|
||||||
|
|
||||||
if(instanceName.EndsWith(' '))
|
if (instanceName.EndsWith(' ') || instanceName.EndsWith('.'))
|
||||||
return "The instance name must not end with a space.";
|
return "The instance name must not end with a space or a dot.";
|
||||||
|
|
||||||
|
if (instanceName.StartsWith('-') || instanceName.StartsWith('_'))
|
||||||
|
return "The instance name must not start with a hyphen or an underscore.";
|
||||||
|
|
||||||
|
if (instanceName.Length > 255)
|
||||||
|
return "The instance name must not exceed 255 characters.";
|
||||||
|
|
||||||
// The instance name must only contain letters, numbers, and spaces:
|
|
||||||
if (!InstanceNameRegex().IsMatch(instanceName))
|
if (!InstanceNameRegex().IsMatch(instanceName))
|
||||||
return "The instance name must only contain letters, numbers, and spaces.";
|
return "The instance name must only contain letters, numbers, spaces, hyphens, underscores, and dots.";
|
||||||
|
|
||||||
if (instanceName.Contains(" "))
|
if (instanceName.Contains(" "))
|
||||||
return "The instance name must not contain consecutive spaces.";
|
return "The instance name must not contain consecutive spaces.";
|
||||||
|
|
||||||
|
if (RESERVED_NAMES.Contains(instanceName.ToUpperInvariant()))
|
||||||
|
return "This name is reserved and cannot be used.";
|
||||||
|
|
||||||
|
if (instanceName.Any(c => Path.GetInvalidFileNameChars().Contains(c)))
|
||||||
|
return "The instance name contains invalid characters.";
|
||||||
|
|
||||||
// The instance name must be unique:
|
// The instance name must be unique:
|
||||||
var lowerInstanceName = instanceName.ToLowerInvariant();
|
var lowerInstanceName = instanceName.ToLowerInvariant();
|
||||||
if (lowerInstanceName != this.dataEditingPreviousInstanceName && this.UsedInstanceNames.Contains(lowerInstanceName))
|
if (lowerInstanceName != this.dataEditingPreviousInstanceName && this.UsedInstanceNames.Contains(lowerInstanceName))
|
||||||
@ -206,6 +237,9 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
|
|
||||||
private string? ValidatingAPIKey(string apiKey)
|
private string? ValidatingAPIKey(string apiKey)
|
||||||
{
|
{
|
||||||
|
if(this.DataProvider is Providers.SELF_HOSTED)
|
||||||
|
return null;
|
||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(this.dataAPIKeyStorageIssue))
|
if(!string.IsNullOrWhiteSpace(this.dataAPIKeyStorageIssue))
|
||||||
return this.dataAPIKeyStorageIssue;
|
return this.dataAPIKeyStorageIssue;
|
||||||
|
|
||||||
@ -215,13 +249,28 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Cancel() => this.MudDialog.Cancel();
|
private string? ValidatingHostname(string hostname)
|
||||||
|
{
|
||||||
|
if(this.DataProvider != Providers.SELF_HOSTED)
|
||||||
|
return null;
|
||||||
|
|
||||||
private bool CanLoadModels => !string.IsNullOrWhiteSpace(this.dataAPIKey) && this.DataProvider != Providers.NONE && !string.IsNullOrWhiteSpace(this.DataInstanceName);
|
if(string.IsNullOrWhiteSpace(hostname))
|
||||||
|
return "Please enter a hostname, e.g., http://localhost:1234";
|
||||||
|
|
||||||
|
if(!hostname.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !hostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
return "The hostname must start with either http:// or https://";
|
||||||
|
|
||||||
|
if(!Uri.TryCreate(hostname, UriKind.Absolute, out _))
|
||||||
|
return "The hostname is not a valid HTTP(S) URL.";
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel() => this.MudDialog.Cancel();
|
||||||
|
|
||||||
private async Task ReloadModels()
|
private async Task ReloadModels()
|
||||||
{
|
{
|
||||||
var provider = this.DataProvider.CreateProvider(this.DataInstanceName);
|
var provider = this.DataProvider.CreateProvider("temp");
|
||||||
if(provider is NoProvider)
|
if(provider is NoProvider)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -233,4 +282,19 @@ public partial class ProviderDialog : ComponentBase
|
|||||||
this.availableModels.Clear();
|
this.availableModels.Clear();
|
||||||
this.availableModels.AddRange(orderedModels);
|
this.availableModels.AddRange(orderedModels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanLoadModels => !string.IsNullOrWhiteSpace(this.dataAPIKey) && this.DataProvider != Providers.NONE && this.DataProvider != Providers.SELF_HOSTED;
|
||||||
|
|
||||||
|
private bool IsCloudProvider => this.DataProvider is not Providers.SELF_HOSTED;
|
||||||
|
|
||||||
|
private bool IsSelfHostedOrNone => this.DataProvider is Providers.SELF_HOSTED or Providers.NONE;
|
||||||
|
|
||||||
|
private string GetProviderCreationURL() => this.DataProvider switch
|
||||||
|
{
|
||||||
|
Providers.OPEN_AI => "https://platform.openai.com/signup",
|
||||||
|
Providers.MISTRAL => "https://console.mistral.ai/",
|
||||||
|
Providers.ANTHROPIC => "https://console.anthropic.com/dashboard",
|
||||||
|
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
}
|
}
|
@ -103,7 +103,7 @@ public sealed class SettingsManager
|
|||||||
if(loadedConfiguration is null)
|
if(loadedConfiguration is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.ConfigurationData = loadedConfiguration;
|
this.ConfigurationData = SettingsMigrations.Migrate(loadedConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
39
app/MindWork AI Studio/Settings/SettingsMigrations.cs
Normal file
39
app/MindWork AI Studio/Settings/SettingsMigrations.cs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
namespace AIStudio.Settings;
|
||||||
|
|
||||||
|
public static class SettingsMigrations
|
||||||
|
{
|
||||||
|
public static Data Migrate(Data previousData)
|
||||||
|
{
|
||||||
|
switch (previousData.Version)
|
||||||
|
{
|
||||||
|
case Version.V1:
|
||||||
|
return MigrateFromV1(previousData);
|
||||||
|
|
||||||
|
default:
|
||||||
|
Console.WriteLine("No migration needed.");
|
||||||
|
return previousData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Data MigrateFromV1(Data previousData)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Summary:
|
||||||
|
// In v1 we had no self-hosted providers. Thus, we had no hostnames.
|
||||||
|
//
|
||||||
|
|
||||||
|
Console.WriteLine("Migrating from v1 to v2...");
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Version = Version.V2,
|
||||||
|
|
||||||
|
Providers = previousData.Providers.Select(provider => provider with { IsSelfHosted = false, Hostname = "" }).ToList(),
|
||||||
|
|
||||||
|
EnableSpellchecking = previousData.EnableSpellchecking,
|
||||||
|
IsSavingEnergy = previousData.IsSavingEnergy,
|
||||||
|
NextProviderNum = previousData.NextProviderNum,
|
||||||
|
ShortcutSendBehavior = previousData.ShortcutSendBehavior,
|
||||||
|
UpdateBehavior = previousData.UpdateBehavior,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -7,5 +7,7 @@ namespace AIStudio.Settings;
|
|||||||
public enum Version
|
public enum Version
|
||||||
{
|
{
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
|
|
||||||
V1,
|
V1,
|
||||||
|
V2,
|
||||||
}
|
}
|
8
app/MindWork AI Studio/wwwroot/changelog/v0.6.3.md
Normal file
8
app/MindWork AI Studio/wwwroot/changelog/v0.6.3.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# v0.6.3, build 159 (2024-07-03 18:26 UTC)
|
||||||
|
- Added possibility to configure a self-hosted or local provider.
|
||||||
|
- Added settings migration processor to handle settings migration from previous versions.
|
||||||
|
- Added links to create an account for the selected provider in the provider dialog.
|
||||||
|
- Added links to each provider's dashboard to the settings page.
|
||||||
|
- Added self-hosted and local providers.
|
||||||
|
- Improved instance name validation.
|
||||||
|
- Optimized the provider dialog: changed the layout to improve usability.
|
@ -1,9 +1,9 @@
|
|||||||
0.6.2
|
0.6.3
|
||||||
2024-07-01 18:08:01 UTC
|
2024-07-03 18:26:31 UTC
|
||||||
158
|
159
|
||||||
8.0.206 (commit bb12410699)
|
8.0.206 (commit bb12410699)
|
||||||
8.0.6 (commit 3b8b000a0e)
|
8.0.6 (commit 3b8b000a0e)
|
||||||
1.79.0 (commit 129f3b996)
|
1.79.0 (commit 129f3b996)
|
||||||
6.20.0
|
6.20.0
|
||||||
1.6.1
|
1.6.1
|
||||||
c86a9e32c12, release
|
ac6748e9eb5, release
|
||||||
|
2
runtime/Cargo.lock
generated
2
runtime/Cargo.lock
generated
@ -2313,7 +2313,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mindwork-ai-studio"
|
name = "mindwork-ai-studio"
|
||||||
version = "0.6.2"
|
version = "0.6.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"flexi_logger",
|
"flexi_logger",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mindwork-ai-studio"
|
name = "mindwork-ai-studio"
|
||||||
version = "0.6.2"
|
version = "0.6.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "MindWork AI Studio"
|
description = "MindWork AI Studio"
|
||||||
authors = ["Thorsten Sommer"]
|
authors = ["Thorsten Sommer"]
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "MindWork AI Studio",
|
"productName": "MindWork AI Studio",
|
||||||
"version": "0.6.2"
|
"version": "0.6.3"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
|
Loading…
Reference in New Issue
Block a user