mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-04-02 13:51:36 +00:00
Merge branch 'main' into Token
This commit is contained in:
commit
40dc6fde99
@ -2173,9 +2173,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"]
|
|||||||
-- Administration settings are not visible
|
-- Administration settings are not visible
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Administration settings are not visible"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Administration settings are not visible"
|
||||||
|
|
||||||
|
-- Embedding Result
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result"
|
||||||
|
|
||||||
-- Delete
|
-- Delete
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete"
|
||||||
|
|
||||||
|
-- Embed text
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1644934561"] = "Embed text"
|
||||||
|
|
||||||
|
-- Test Embedding Provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Test Embedding Provider"
|
||||||
|
|
||||||
-- Add Embedding
|
-- Add Embedding
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding"
|
||||||
|
|
||||||
@ -2188,6 +2197,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253
|
|||||||
-- Add Embedding Provider
|
-- Add Embedding Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider"
|
||||||
|
|
||||||
|
-- Add text that should be embedded:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Add text that should be embedded:"
|
||||||
|
|
||||||
|
-- Embedding Vector (one dimension per line)
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Embedding Vector (one dimension per line)"
|
||||||
|
|
||||||
-- Model
|
-- Model
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model"
|
||||||
|
|
||||||
@ -2197,6 +2212,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199
|
|||||||
-- Name
|
-- Name
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name"
|
||||||
|
|
||||||
|
-- No embedding was returned.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "No embedding was returned."
|
||||||
|
|
||||||
-- Configured Embedding Providers
|
-- Configured Embedding Providers
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers"
|
||||||
|
|
||||||
@ -2206,6 +2224,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512
|
|||||||
-- Edit
|
-- Edit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit"
|
||||||
|
|
||||||
|
-- Close
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Close"
|
||||||
|
|
||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions"
|
||||||
|
|
||||||
@ -2227,6 +2248,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130
|
|||||||
-- Open Dashboard
|
-- Open Dashboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard"
|
||||||
|
|
||||||
|
-- Test
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Test"
|
||||||
|
|
||||||
|
-- Example text to embed
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Example text to embed"
|
||||||
|
|
||||||
-- Provider
|
-- Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider"
|
||||||
|
|
||||||
@ -3331,6 +3358,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Pro
|
|||||||
-- Cancel
|
-- Cancel
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel"
|
||||||
|
|
||||||
|
-- Embedding Vector
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embedding Vector"
|
||||||
|
|
||||||
|
-- Close
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close"
|
||||||
|
|
||||||
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
|
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system."
|
||||||
|
|
||||||
|
|||||||
@ -96,10 +96,10 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudMarkdown Value="@textContent.Text.RemoveThinkTags().Trim()" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" />
|
<MudMarkdown Value="@NormalizeMarkdownForRendering(textContent.Text)" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE" />
|
||||||
@if (textContent.Sources.Count > 0)
|
@if (textContent.Sources.Count > 0)
|
||||||
{
|
{
|
||||||
<MudMarkdown Value="@textContent.Sources.ToMarkdown()" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" />
|
<MudMarkdown Value="@textContent.Sources.ToMarkdown()" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,18 @@ namespace AIStudio.Chat;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class ContentBlockComponent : MSGComponentBase
|
public partial class ContentBlockComponent : MSGComponentBase
|
||||||
{
|
{
|
||||||
|
private static readonly string[] HTML_TAG_MARKERS =
|
||||||
|
[
|
||||||
|
"<!doctype",
|
||||||
|
"<html",
|
||||||
|
"<head",
|
||||||
|
"<body",
|
||||||
|
"<style",
|
||||||
|
"<script",
|
||||||
|
"<iframe",
|
||||||
|
"<svg",
|
||||||
|
];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The role of the chat content block.
|
/// The role of the chat content block.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -68,18 +80,37 @@ public partial class ContentBlockComponent : MSGComponentBase
|
|||||||
private RustService RustService { get; init; } = null!;
|
private RustService RustService { get; init; } = null!;
|
||||||
|
|
||||||
private bool HideContent { get; set; }
|
private bool HideContent { get; set; }
|
||||||
|
private bool hasRenderHash;
|
||||||
|
private int lastRenderHash;
|
||||||
|
|
||||||
#region Overrides of ComponentBase
|
#region Overrides of ComponentBase
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
// Register the streaming events:
|
this.RegisterStreamingEvents();
|
||||||
this.Content.StreamingDone = this.AfterStreaming;
|
|
||||||
this.Content.StreamingEvent = () => this.InvokeAsync(this.StateHasChanged);
|
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override Task OnParametersSetAsync()
|
||||||
|
{
|
||||||
|
this.RegisterStreamingEvents();
|
||||||
|
return base.OnParametersSetAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool ShouldRender()
|
||||||
|
{
|
||||||
|
var currentRenderHash = this.CreateRenderHash();
|
||||||
|
if (!this.hasRenderHash || currentRenderHash != this.lastRenderHash)
|
||||||
|
{
|
||||||
|
this.lastRenderHash = currentRenderHash;
|
||||||
|
this.hasRenderHash = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets called when the content stream ended.
|
/// Gets called when the content stream ended.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -111,6 +142,47 @@ public partial class ContentBlockComponent : MSGComponentBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RegisterStreamingEvents()
|
||||||
|
{
|
||||||
|
this.Content.StreamingDone = this.AfterStreaming;
|
||||||
|
this.Content.StreamingEvent = () => this.InvokeAsync(this.StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CreateRenderHash()
|
||||||
|
{
|
||||||
|
var hash = new HashCode();
|
||||||
|
hash.Add(this.Role);
|
||||||
|
hash.Add(this.Type);
|
||||||
|
hash.Add(this.Time);
|
||||||
|
hash.Add(this.Class);
|
||||||
|
hash.Add(this.IsLastContentBlock);
|
||||||
|
hash.Add(this.IsSecondToLastBlock);
|
||||||
|
hash.Add(this.HideContent);
|
||||||
|
hash.Add(this.SettingsManager.IsDarkMode);
|
||||||
|
hash.Add(this.RegenerateEnabled());
|
||||||
|
hash.Add(this.Content.InitialRemoteWait);
|
||||||
|
hash.Add(this.Content.IsStreaming);
|
||||||
|
hash.Add(this.Content.FileAttachments.Count);
|
||||||
|
hash.Add(this.Content.Sources.Count);
|
||||||
|
|
||||||
|
switch (this.Content)
|
||||||
|
{
|
||||||
|
case ContentText text:
|
||||||
|
var textValue = text.Text;
|
||||||
|
hash.Add(textValue.Length);
|
||||||
|
hash.Add(textValue.GetHashCode(StringComparison.Ordinal));
|
||||||
|
hash.Add(text.Sources.Count);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ContentImage image:
|
||||||
|
hash.Add(image.SourceType);
|
||||||
|
hash.Add(image.Source);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash.ToHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private string CardClasses => $"my-2 rounded-lg {this.Class}";
|
private string CardClasses => $"my-2 rounded-lg {this.Class}";
|
||||||
@ -122,6 +194,34 @@ public partial class ContentBlockComponent : MSGComponentBase
|
|||||||
CodeBlock = { Theme = this.CodeColorPalette },
|
CodeBlock = { Theme = this.CodeColorPalette },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string NormalizeMarkdownForRendering(string text)
|
||||||
|
{
|
||||||
|
var cleaned = text.RemoveThinkTags().Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(cleaned))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (cleaned.Contains("```", StringComparison.Ordinal))
|
||||||
|
return cleaned;
|
||||||
|
|
||||||
|
if (LooksLikeRawHtml(cleaned))
|
||||||
|
return $"```html{Environment.NewLine}{cleaned}{Environment.NewLine}```";
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeRawHtml(string text)
|
||||||
|
{
|
||||||
|
var content = text.TrimStart();
|
||||||
|
if (!content.StartsWith("<", StringComparison.Ordinal))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach (var marker in HTML_TAG_MARKERS)
|
||||||
|
if (content.Contains(marker, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return content.Contains("</", StringComparison.Ordinal) || content.Contains("/>", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RemoveBlock()
|
private async Task RemoveBlock()
|
||||||
{
|
{
|
||||||
if (this.RemoveBlockFunc is null)
|
if (this.RemoveBlockFunc is null)
|
||||||
|
|||||||
@ -13,6 +13,7 @@ public partial class Changelog
|
|||||||
|
|
||||||
public static readonly Log[] LOGS =
|
public static readonly Log[] LOGS =
|
||||||
[
|
[
|
||||||
|
new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"),
|
||||||
new (233, "v26.2.1, build 233 (2026-02-01 19:16 UTC)", "v26.2.1.md"),
|
new (233, "v26.2.1, build 233 (2026-02-01 19:16 UTC)", "v26.2.1.md"),
|
||||||
new (232, "v26.1.2, build 232 (2026-01-25 14:05 UTC)", "v26.1.2.md"),
|
new (232, "v26.1.2, build 232 (2026-01-25 14:05 UTC)", "v26.1.2.md"),
|
||||||
new (231, "v26.1.1, build 231 (2026-01-11 15:53 UTC)", "v26.1.1.md"),
|
new (231, "v26.1.1, build 231 (2026-01-11 15:53 UTC)", "v26.1.1.md"),
|
||||||
|
|||||||
@ -6,4 +6,4 @@
|
|||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
|
|
||||||
<MudMarkdown Value="@this.LogContent" Props="Markdown.DefaultConfig"/>
|
<MudMarkdown Value="@this.LogContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||||
@ -16,6 +16,7 @@
|
|||||||
@if (!block.HideFromUser)
|
@if (!block.HideFromUser)
|
||||||
{
|
{
|
||||||
<ContentBlockComponent
|
<ContentBlockComponent
|
||||||
|
@key="@block"
|
||||||
Role="@block.Role"
|
Role="@block.Role"
|
||||||
Type="@block.ContentType"
|
Type="@block.ContentType"
|
||||||
Time="@block.Time"
|
Time="@block.Time"
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
<MudText Typo="Typo.h6">
|
<MudText Typo="Typo.h6">
|
||||||
@T("Description")
|
@T("Description")
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudMarkdown Value="@this.currentConfidence.Description"/>
|
<MudMarkdown Value="@this.currentConfidence.Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||||
|
|
||||||
@if (this.currentConfidence.Sources.Count > 0)
|
@if (this.currentConfidence.Sources.Count > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
10
app/MindWork AI Studio/Components/ConfigInfoRow.razor
Normal file
10
app/MindWork AI Studio/Components/ConfigInfoRow.razor
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<div style="display: flex; align-items: center; gap: 8px; @this.Item.Style">
|
||||||
|
<MudIcon Icon="@this.Item.Icon"/>
|
||||||
|
<span>
|
||||||
|
@this.Item.Text
|
||||||
|
</span>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(this.Item.CopyValue))
|
||||||
|
{
|
||||||
|
<MudCopyClipboardButton TooltipMessage="@this.Item.CopyTooltip" StringContent="@this.Item.CopyValue"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
9
app/MindWork AI Studio/Components/ConfigInfoRow.razor.cs
Normal file
9
app/MindWork AI Studio/Components/ConfigInfoRow.razor.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
public partial class ConfigInfoRow : ComponentBase
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public ConfigInfoRowItem Item { get; set; } = new(Icons.Material.Filled.ArrowRightAlt, string.Empty, string.Empty, string.Empty, string.Empty);
|
||||||
|
}
|
||||||
9
app/MindWork AI Studio/Components/ConfigInfoRowItem.cs
Normal file
9
app/MindWork AI Studio/Components/ConfigInfoRowItem.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
public sealed record ConfigInfoRowItem(
|
||||||
|
string Icon,
|
||||||
|
string Text,
|
||||||
|
string CopyValue,
|
||||||
|
string CopyTooltip,
|
||||||
|
string Style = ""
|
||||||
|
);
|
||||||
21
app/MindWork AI Studio/Components/ConfigPluginInfoCard.razor
Normal file
21
app/MindWork AI Studio/Components/ConfigPluginInfoCard.razor
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<MudPaper Outlined="true" Class="@this.Class">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||||
|
<MudIcon Icon="@this.HeaderIcon" Size="Size.Small"/>
|
||||||
|
<MudText Typo="Typo.subtitle2">
|
||||||
|
@this.HeaderText
|
||||||
|
</MudText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@foreach (var item in this.Items)
|
||||||
|
{
|
||||||
|
<ConfigInfoRow Item="@item"/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (this.ShowWarning)
|
||||||
|
{
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Warning" Size="Size.Small" Color="Color.Warning"/>
|
||||||
|
<MudText Typo="Typo.subtitle2">@this.WarningText</MudText>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
public partial class ConfigPluginInfoCard : ComponentBase
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public string HeaderIcon { get; set; } = Icons.Material.Filled.Extension;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string HeaderText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IEnumerable<ConfigInfoRowItem> Items { get; set; } = [];
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowWarning { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string WarningText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Class { get; set; } = "pa-3 mt-2 mb-2";
|
||||||
|
}
|
||||||
@ -14,8 +14,22 @@
|
|||||||
SelectedValuesChanged="@this.OptionChanged">
|
SelectedValuesChanged="@this.OptionChanged">
|
||||||
@foreach (var data in this.Data)
|
@foreach (var data in this.Data)
|
||||||
{
|
{
|
||||||
<MudSelectItemExtended Value="@data.Value">
|
var isLockedValue = this.IsLockedValue(data.Value);
|
||||||
|
<MudSelectItemExtended Value="@data.Value" Disabled="@isLockedValue" Style="@(isLockedValue ? "pointer-events:auto !important;" : null)">
|
||||||
|
@if (isLockedValue)
|
||||||
|
{
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.FlexStart" Wrap="Wrap.NoWrap">
|
||||||
|
@* MudTooltip.RootStyle is set as a workaround for issue -> https://github.com/MudBlazor/MudBlazor/issues/10882 *@
|
||||||
|
<MudTooltip Text="@this.LockedTooltip()" Arrow="true" Placement="Placement.Right" RootStyle="display:inline-flex;">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" Size="Size.Small" Class="mr-1"/>
|
||||||
|
</MudTooltip>
|
||||||
@data.Name
|
@data.Name
|
||||||
|
</MudStack>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@data.Name
|
||||||
|
}
|
||||||
</MudSelectItemExtended>
|
</MudSelectItemExtended>
|
||||||
}
|
}
|
||||||
</MudSelectExtended>
|
</MudSelectExtended>
|
||||||
@ -28,6 +28,12 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public Action<HashSet<TData>> SelectionUpdate { get; set; } = _ => { };
|
public Action<HashSet<TData>> SelectionUpdate { get; set; } = _ => { };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether a specific item is locked by a configuration plugin.
|
||||||
|
/// </summary>
|
||||||
|
[Parameter]
|
||||||
|
public Func<TData, bool> IsItemLocked { get; set; } = _ => false;
|
||||||
|
|
||||||
#region Overrides of ConfigurationBase
|
#region Overrides of ConfigurationBase
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -62,4 +68,12 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
|
|||||||
|
|
||||||
return string.Format(T("You have selected {0} preview features."), selectedValues.Count);
|
return string.Format(T("You have selected {0} preview features."), selectedValues.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsLockedValue(TData value) => this.IsItemLocked(value);
|
||||||
|
|
||||||
|
private string LockedTooltip() =>
|
||||||
|
this.T(
|
||||||
|
"This feature is managed by your organization and has therefore been disabled.",
|
||||||
|
typeof(ConfigurationBase).Namespace,
|
||||||
|
nameof(ConfigurationBase));
|
||||||
}
|
}
|
||||||
15
app/MindWork AI Studio/Components/EncryptionSecretInfo.razor
Normal file
15
app/MindWork AI Studio/Components/EncryptionSecretInfo.razor
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<MudText Typo="Typo.body1" Class="@this.Class">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
||||||
|
@if (this.IsConfigured)
|
||||||
|
{
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small"/>
|
||||||
|
<span>@this.ConfiguredText</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Error" Size="Size.Small"/>
|
||||||
|
<span>@this.NotConfiguredText</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</MudText>
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Components;
|
||||||
|
|
||||||
|
public partial class EncryptionSecretInfo : ComponentBase
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public bool IsConfigured { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ConfiguredText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string NotConfiguredText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string Class { get; set; } = "mt-2 mb-2";
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@
|
|||||||
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
|
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="profile">
|
<MudSelectItem Value="profile">
|
||||||
@profile.Name
|
@profile.GetSafeName()
|
||||||
</MudSelectItem>
|
</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
|
|||||||
@ -25,11 +25,11 @@
|
|||||||
var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList();
|
var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList();
|
||||||
if (availablePreviewFeatures.Count > 0)
|
if (availablePreviewFeatures.Count > 0)
|
||||||
{
|
{
|
||||||
<ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@(() => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet())" Data="@availablePreviewFeatures" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedValue)" OptionHelp="@T("Which preview features would you like to enable?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked"/>
|
<ConfigurationMultiSelect OptionDescription="@T("Select preview features")" SelectedValues="@this.GetSelectedPreviewFeatures" Data="@availablePreviewFeatures" SelectionUpdate="@this.UpdateEnabledPreviewFeatures" OptionHelp="@T("Which preview features would you like to enable?")" IsItemLocked="@this.IsPluginContributedPreviewFeature" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.IsLocked"/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => T("Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence."))"/>
|
<ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => T("Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence."))" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
|
||||||
<ConfigurationSelect OptionDescription="@T("Preselect one of your profiles?")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="@T("Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/>
|
<ConfigurationSelect OptionDescription="@T("Preselect one of your profiles?")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="@T("Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/>
|
||||||
|
|
||||||
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||||
|
|||||||
@ -27,7 +27,41 @@ public partial class SettingsPanelApp : SettingsPanelBase
|
|||||||
private void UpdatePreviewFeatures(PreviewVisibility previewVisibility)
|
private void UpdatePreviewFeatures(PreviewVisibility previewVisibility)
|
||||||
{
|
{
|
||||||
this.SettingsManager.ConfigurationData.App.PreviewVisibility = previewVisibility;
|
this.SettingsManager.ConfigurationData.App.PreviewVisibility = previewVisibility;
|
||||||
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = previewVisibility.FilterPreviewFeatures(this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures);
|
var filtered = previewVisibility.FilterPreviewFeatures(this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures);
|
||||||
|
filtered.UnionWith(this.GetPluginContributedPreviewFeatures());
|
||||||
|
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashSet<PreviewFeatures> GetPluginContributedPreviewFeatures()
|
||||||
|
{
|
||||||
|
if (ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) && meta.HasPluginContribution)
|
||||||
|
return meta.PluginContribution.Where(x => !x.IsReleased()).ToHashSet();
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPluginContributedPreviewFeature(PreviewFeatures feature)
|
||||||
|
{
|
||||||
|
if (feature.IsReleased())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!ManagedConfiguration.TryGet(x => x.App, x => x.EnabledPreviewFeatures, out var meta) || !meta.HasPluginContribution)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return meta.PluginContribution.Contains(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashSet<PreviewFeatures> GetSelectedPreviewFeatures()
|
||||||
|
{
|
||||||
|
var enabled = this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures.Where(x => !x.IsReleased()).ToHashSet();
|
||||||
|
enabled.UnionWith(this.GetPluginContributedPreviewFeatures());
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateEnabledPreviewFeatures(HashSet<PreviewFeatures> selectedFeatures)
|
||||||
|
{
|
||||||
|
selectedFeatures.UnionWith(this.GetPluginContributedPreviewFeatures());
|
||||||
|
this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedFeatures;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task UpdateLangBehaviour(LangBehavior behavior)
|
private async Task UpdateLangBehaviour(LangBehavior behavior)
|
||||||
|
|||||||
@ -53,12 +53,18 @@
|
|||||||
<MudTooltip Text="@T("Edit")">
|
<MudTooltip Text="@T("Edit")">
|
||||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditEmbeddingProvider(context))"/>
|
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditEmbeddingProvider(context))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||||
|
{
|
||||||
<MudTooltip Text="@T("Export configuration")">
|
<MudTooltip Text="@T("Export configuration")">
|
||||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportEmbeddingProvider(context))"/>
|
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportEmbeddingProvider(context))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
}
|
||||||
<MudTooltip Text="@T("Delete")">
|
<MudTooltip Text="@T("Delete")">
|
||||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteEmbeddingProvider(context))"/>
|
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteEmbeddingProvider(context))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
<MudTooltip Text="@T("Test")">
|
||||||
|
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Api" OnClick="@(() => this.TestEmbeddingProvider(context))"/>
|
||||||
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
|
using System.Globalization;
|
||||||
using AIStudio.Dialogs;
|
using AIStudio.Dialogs;
|
||||||
|
using AIStudio.Provider;
|
||||||
using AIStudio.Settings;
|
using AIStudio.Settings;
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
@ -117,6 +119,9 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
|
|||||||
|
|
||||||
private async Task ExportEmbeddingProvider(EmbeddingProvider provider)
|
private async Task ExportEmbeddingProvider(EmbeddingProvider provider)
|
||||||
{
|
{
|
||||||
|
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||||
|
return;
|
||||||
|
|
||||||
if (provider == EmbeddingProvider.NONE)
|
if (provider == EmbeddingProvider.NONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -131,4 +136,48 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
|
|||||||
|
|
||||||
await this.AvailableEmbeddingProvidersChanged.InvokeAsync(this.AvailableEmbeddingProviders);
|
await this.AvailableEmbeddingProvidersChanged.InvokeAsync(this.AvailableEmbeddingProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task TestEmbeddingProvider(EmbeddingProvider provider)
|
||||||
|
{
|
||||||
|
var dialogParameters = new DialogParameters<SingleInputDialog>
|
||||||
|
{
|
||||||
|
{ x => x.ConfirmText, T("Embed text") },
|
||||||
|
{ x => x.InputHeaderText, T("Add text that should be embedded:") },
|
||||||
|
{ x => x.UserInput, T("Example text to embed") },
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialogReference = await this.DialogService.ShowAsync<SingleInputDialog>(T("Test Embedding Provider"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||||
|
var dialogResult = await dialogReference.Result;
|
||||||
|
if (dialogResult is null || dialogResult.Canceled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var inputText = dialogResult.Data as string;
|
||||||
|
if (string.IsNullOrWhiteSpace(inputText))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var embeddingProvider = provider.CreateProvider();
|
||||||
|
var embeddings = await embeddingProvider.EmbedTextAsync(provider.Model, this.SettingsManager, default, new List<string> { inputText });
|
||||||
|
|
||||||
|
if (embeddings.Count == 0)
|
||||||
|
{
|
||||||
|
await this.DialogService.ShowMessageBox(T("Embedding Result"), T("No embedding was returned."), T("Close"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var vector = embeddings.FirstOrDefault();
|
||||||
|
if (vector is null || vector.Count == 0)
|
||||||
|
{
|
||||||
|
await this.DialogService.ShowMessageBox(T("Embedding Result"), T("No embedding was returned."), T("Close"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultText = string.Join(Environment.NewLine, vector.Select(value => value.ToString("G9", CultureInfo.InvariantCulture)));
|
||||||
|
var resultParameters = new DialogParameters<EmbeddingResultDialog>
|
||||||
|
{
|
||||||
|
{ x => x.ResultText, resultText },
|
||||||
|
{ x => x.ResultLabel, T("Embedding Vector (one dimension per line)") },
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.DialogService.ShowAsync<EmbeddingResultDialog>(T("Embedding Result"), resultParameters, DialogOptions.FULLSCREEN);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,9 +45,12 @@
|
|||||||
<MudTooltip Text="@T("Edit")">
|
<MudTooltip Text="@T("Edit")">
|
||||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditLLMProvider(context))"/>
|
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditLLMProvider(context))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||||
|
{
|
||||||
<MudTooltip Text="@T("Export configuration")">
|
<MudTooltip Text="@T("Export configuration")">
|
||||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportLLMProvider(context))"/>
|
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportLLMProvider(context))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
}
|
||||||
<MudTooltip Text="@T("Delete")">
|
<MudTooltip Text="@T("Delete")">
|
||||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteLLMProvider(context))"/>
|
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteLLMProvider(context))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
@ -101,7 +104,7 @@
|
|||||||
@context.ToName()
|
@context.ToName()
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd>
|
<MudTd>
|
||||||
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description"/>
|
<MudMarkdown Value="@context.GetConfidence(this.SettingsManager).Description" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||||
</MudTd>
|
</MudTd>
|
||||||
<MudTd Style="vertical-align: top;">
|
<MudTd Style="vertical-align: top;">
|
||||||
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)">
|
<MudMenu StartIcon="@Icons.Material.Filled.Security" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.GetCurrentConfidenceLevelName(context)" Variant="Variant.Filled" Style="@this.SetCurrentConfidenceLevelColorStyle(context)">
|
||||||
|
|||||||
@ -136,6 +136,9 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
|
|||||||
|
|
||||||
private async Task ExportLLMProvider(AIStudio.Settings.Provider provider)
|
private async Task ExportLLMProvider(AIStudio.Settings.Provider provider)
|
||||||
{
|
{
|
||||||
|
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||||
|
return;
|
||||||
|
|
||||||
if (provider == AIStudio.Settings.Provider.NONE)
|
if (provider == AIStudio.Settings.Provider.NONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@ -50,9 +50,12 @@
|
|||||||
<MudTooltip Text="@T("Edit")">
|
<MudTooltip Text="@T("Edit")">
|
||||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditTranscriptionProvider(context))"/>
|
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditTranscriptionProvider(context))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||||
|
{
|
||||||
<MudTooltip Text="@T("Export configuration")">
|
<MudTooltip Text="@T("Export configuration")">
|
||||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportTranscriptionProvider(context))"/>
|
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportTranscriptionProvider(context))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
}
|
||||||
<MudTooltip Text="@T("Delete")">
|
<MudTooltip Text="@T("Delete")">
|
||||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteTranscriptionProvider(context))"/>
|
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteTranscriptionProvider(context))"/>
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
|
|||||||
@ -117,6 +117,9 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase
|
|||||||
|
|
||||||
private async Task ExportTranscriptionProvider(TranscriptionProvider provider)
|
private async Task ExportTranscriptionProvider(TranscriptionProvider provider)
|
||||||
{
|
{
|
||||||
|
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||||
|
return;
|
||||||
|
|
||||||
if (provider == TranscriptionProvider.NONE)
|
if (provider == TranscriptionProvider.NONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
Class="ma-2 pe-4"
|
Class="ma-2 pe-4"
|
||||||
HelperText="@T("This is the content we loaded from your file — including headings, lists, and formatting. Use this to verify your file loads as expected.")">
|
HelperText="@T("This is the content we loaded from your file — including headings, lists, and formatting. Use this to verify your file loads as expected.")">
|
||||||
<div style="max-height: 40vh; overflow-y: auto;">
|
<div style="max-height: 40vh; overflow-y: auto;">
|
||||||
<MudMarkdown Value="@this.FileContent" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling"/>
|
<MudMarkdown Value="@this.FileContent" Props="Markdown.DefaultConfig" Styling="@this.MarkdownStyling" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||||
</div>
|
</div>
|
||||||
</MudField>
|
</MudField>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
|
|||||||
22
app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor
Normal file
22
app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
@inherits MSGComponentBase
|
||||||
|
|
||||||
|
<MudDialog>
|
||||||
|
<DialogContent>
|
||||||
|
<MudTextField
|
||||||
|
T="string"
|
||||||
|
Text="@this.ResultText"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Lines="10"
|
||||||
|
AutoGrow="@true"
|
||||||
|
MaxLines="25"
|
||||||
|
ReadOnly="true"
|
||||||
|
Label="@this.ResultLabelText"
|
||||||
|
AdornmentIcon="@Icons.Material.Filled.DataObject"
|
||||||
|
Adornment="Adornment.Start"/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton OnClick="@this.Close" Variant="Variant.Filled" Color="Color.Primary">
|
||||||
|
@T("Close")
|
||||||
|
</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
using AIStudio.Components;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace AIStudio.Dialogs;
|
||||||
|
|
||||||
|
public partial class EmbeddingResultDialog : MSGComponentBase
|
||||||
|
{
|
||||||
|
[CascadingParameter]
|
||||||
|
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ResultText { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string ResultLabel { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private string ResultLabelText => string.IsNullOrWhiteSpace(this.ResultLabel) ? T("Embedding Vector") : this.ResultLabel;
|
||||||
|
|
||||||
|
private void Close() => this.MudDialog.Close();
|
||||||
|
}
|
||||||
@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(this.licenseText))
|
else if (!string.IsNullOrWhiteSpace(this.licenseText))
|
||||||
{
|
{
|
||||||
<MudMarkdown Value="@this.licenseText" Props="Markdown.DefaultConfig"/>
|
<MudMarkdown Value="@this.licenseText" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||||
}
|
}
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<MudIcon Icon="@Icons.Material.Filled.Update" Size="Size.Large" Class="mr-3"/>
|
<MudIcon Icon="@Icons.Material.Filled.Update" Size="Size.Large" Class="mr-3"/>
|
||||||
@this.HeaderText
|
@this.HeaderText
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudMarkdown Value="@this.UpdateResponse.Changelog" Props="Markdown.DefaultConfig"/>
|
<MudMarkdown Value="@this.UpdateResponse.Changelog" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||||
|
|||||||
@ -215,8 +215,28 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
|||||||
.CheckDeferredMessages<EnterpriseEnvironment>(Event.STARTUP_ENTERPRISE_ENVIRONMENT)
|
.CheckDeferredMessages<EnterpriseEnvironment>(Event.STARTUP_ENTERPRISE_ENVIRONMENT)
|
||||||
.Where(env => env != default)
|
.Where(env => env != default)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var failedDeferredConfigIds = new HashSet<Guid>();
|
||||||
foreach (var env in enterpriseEnvironments)
|
foreach (var env in enterpriseEnvironments)
|
||||||
await PluginFactory.TryDownloadingConfigPluginAsync(env.ConfigurationId, env.ConfigurationServerUrl);
|
{
|
||||||
|
var wasDownloadSuccessful = await PluginFactory.TryDownloadingConfigPluginAsync(env.ConfigurationId, env.ConfigurationServerUrl);
|
||||||
|
if (!wasDownloadSuccessful)
|
||||||
|
{
|
||||||
|
failedDeferredConfigIds.Add(env.ConfigurationId);
|
||||||
|
this.Logger.LogWarning("Failed to download deferred enterprise configuration '{ConfigId}' during startup. Keeping managed plugins unchanged.", env.ConfigurationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EnterpriseEnvironmentService.HasValidEnterpriseSnapshot)
|
||||||
|
{
|
||||||
|
var activeConfigIds = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS
|
||||||
|
.Select(env => env.ConfigurationId)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
PluginFactory.RemoveUnreferencedManagedConfigurationPlugins(activeConfigIds);
|
||||||
|
if (failedDeferredConfigIds.Count > 0)
|
||||||
|
this.Logger.LogWarning("Deferred startup updates failed for {FailedCount} enterprise configuration(s). Those configurations were kept unchanged.", failedDeferredConfigIds.Count);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the enterprise encryption service for decrypting API keys:
|
// Initialize the enterprise encryption service for decrypting API keys:
|
||||||
await PluginFactory.InitializeEnterpriseEncryption(this.RustService);
|
await PluginFactory.InitializeEnterpriseEncryption(this.RustService);
|
||||||
|
|||||||
@ -52,7 +52,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.12" />
|
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.12" />
|
||||||
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
||||||
<PackageReference Include="MudBlazor.Markdown" Version="8.11.0" />
|
<PackageReference Include="MudBlazor.Markdown" Version="8.11.0" />
|
||||||
<PackageReference Include="Qdrant.Client" Version="1.16.1" />
|
<PackageReference Include="Qdrant.Client" Version="1.17.0" />
|
||||||
<PackageReference Include="ReverseMarkdown" Version="5.0.0" />
|
<PackageReference Include="ReverseMarkdown" Version="5.0.0" />
|
||||||
<PackageReference Include="LuaCSharp" Version="0.5.3" />
|
<PackageReference Include="LuaCSharp" Version="0.5.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
|
|
||||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")">
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")">
|
||||||
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig"/>
|
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
|
|
||||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")">
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")">
|
||||||
@ -35,7 +35,7 @@
|
|||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
|
|
||||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")">
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")">
|
||||||
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE"/>
|
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
|
|
||||||
</MudExpansionPanels>
|
</MudExpansionPanels>
|
||||||
|
|||||||
@ -64,33 +64,19 @@
|
|||||||
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
|
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
|
||||||
@foreach (var plug in this.configPlugins)
|
@foreach (var plug in this.configPlugins)
|
||||||
{
|
{
|
||||||
<MudPaper Outlined="true" Class="pa-3 mt-2 mb-2">
|
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Extension"
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
HeaderText="@plug.Name"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Extension" Size="Size.Small"/>
|
Items="@([
|
||||||
<MudText Typo="Typo.subtitle2">@plug.Name</MudText>
|
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||||
</div>
|
$"{T("Configuration plugin ID:")} {plug.Id}",
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
plug.Id.ToString(),
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
T("Copies the configuration plugin ID to the clipboard"))
|
||||||
<span>@T("Configuration plugin ID:") @plug.Id</span>
|
])"/>
|
||||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the configuration plugin ID to the clipboard")" StringContent=@plug.Id.ToString()/>
|
|
||||||
</div>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
}
|
||||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
ConfiguredText="@T("Encryption secret: is configured")"
|
||||||
@if (PluginFactory.EnterpriseEncryption?.IsAvailable is true)
|
NotConfiguredText="@T("Encryption secret: is not configured")"/>
|
||||||
{
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small"/>
|
|
||||||
<span>@T("Encryption secret: is configured")</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Error" Size="Size.Small"/>
|
|
||||||
<span>@T("Encryption secret: is not configured")</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</MudText>
|
|
||||||
</MudCollapse>
|
</MudCollapse>
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -101,97 +87,91 @@
|
|||||||
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
|
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
|
||||||
@foreach (var env in EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Where(e => e.IsActive))
|
@foreach (var env in EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Where(e => e.IsActive))
|
||||||
{
|
{
|
||||||
<MudPaper Outlined="true" Class="pa-3 mt-2 mb-2">
|
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
HeaderText="@T("Waiting for the configuration plugin...")"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.HourglassBottom" Size="Size.Small"/>
|
Items="@([
|
||||||
<MudText Typo="Typo.subtitle2">@T("Waiting for the configuration plugin...")</MudText>
|
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||||
</div>
|
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
env.ConfigurationId.ToString(),
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
T("Copies the config ID to the clipboard")),
|
||||||
<span>@T("Enterprise configuration ID:") @env.ConfigurationId</span>
|
|
||||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the config ID to the clipboard")" StringContent=@env.ConfigurationId.ToString()/>
|
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||||
</div>
|
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
|
env.ConfigurationServerUrl,
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
T("Copies the server URL to the clipboard"),
|
||||||
<span>@T("Configuration server:") @env.ConfigurationServerUrl</span>
|
"margin-top: 4px;")
|
||||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the server URL to the clipboard")" StringContent=@env.ConfigurationServerUrl/>
|
])"/>
|
||||||
</div>
|
|
||||||
</MudPaper>
|
|
||||||
}
|
}
|
||||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
ConfiguredText="@T("Encryption secret: is configured")"
|
||||||
@if (PluginFactory.EnterpriseEncryption?.IsAvailable is true)
|
NotConfiguredText="@T("Encryption secret: is not configured")"/>
|
||||||
{
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small"/>
|
|
||||||
<span>@T("Encryption secret: is configured")</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Error" Size="Size.Small"/>
|
|
||||||
<span>@T("Encryption secret: is not configured")</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</MudText>
|
|
||||||
</MudCollapse>
|
</MudCollapse>
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case true:
|
case true:
|
||||||
|
@if (this.HasAnyLoadedEnterpriseConfigurationPlugin)
|
||||||
|
{
|
||||||
<MudText Typo="Typo.body1">
|
<MudText Typo="Typo.body1">
|
||||||
@T("AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active.")
|
@T("AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active.")
|
||||||
</MudText>
|
</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body1">
|
||||||
|
@T("AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are not yet available.")
|
||||||
|
</MudText>
|
||||||
|
}
|
||||||
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
|
<MudCollapse Expanded="@this.showEnterpriseConfigDetails">
|
||||||
@foreach (var env in EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Where(e => e.IsActive))
|
@foreach (var env in EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Where(e => e.IsActive))
|
||||||
{
|
{
|
||||||
var matchingPlugin = this.configPlugins.FirstOrDefault(p => p.Id == env.ConfigurationId);
|
var matchingPlugin = this.FindManagedConfigurationPlugin(env.ConfigurationId);
|
||||||
<MudPaper Outlined="true" Class="pa-3 mt-2 mb-2">
|
if (matchingPlugin is null)
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
|
||||||
@if (matchingPlugin is not null)
|
|
||||||
{
|
{
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Extension" Size="Size.Small"/>
|
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.HourglassBottom"
|
||||||
<MudText Typo="Typo.subtitle2">@matchingPlugin.Name</MudText>
|
HeaderText="@T("Waiting for the configuration plugin...")"
|
||||||
|
Items="@([
|
||||||
|
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||||
|
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
|
||||||
|
env.ConfigurationId.ToString(),
|
||||||
|
T("Copies the config ID to the clipboard")),
|
||||||
|
|
||||||
|
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||||
|
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
|
||||||
|
env.ConfigurationServerUrl,
|
||||||
|
T("Copies the server URL to the clipboard"),
|
||||||
|
"margin-top: 4px;")
|
||||||
|
])"/>
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
<ConfigPluginInfoCard HeaderIcon="@Icons.Material.Filled.Extension"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Warning" Size="Size.Small" Color="Color.Warning"/>
|
HeaderText="@matchingPlugin.Name"
|
||||||
<MudText Typo="Typo.subtitle2">@T("ID mismatch: the plugin ID differs from the enterprise configuration ID.")</MudText>
|
Items="@([
|
||||||
|
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||||
|
$"{T("Enterprise configuration ID:")} {env.ConfigurationId}",
|
||||||
|
env.ConfigurationId.ToString(),
|
||||||
|
T("Copies the config ID to the clipboard")),
|
||||||
|
|
||||||
|
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||||
|
$"{T("Configuration server:")} {env.ConfigurationServerUrl}",
|
||||||
|
env.ConfigurationServerUrl,
|
||||||
|
T("Copies the server URL to the clipboard"),
|
||||||
|
"margin-top: 4px;"),
|
||||||
|
|
||||||
|
new ConfigInfoRowItem(Icons.Material.Filled.ArrowRightAlt,
|
||||||
|
$"{T("Configuration plugin ID:")} {matchingPlugin.Id}",
|
||||||
|
matchingPlugin.Id.ToString(),
|
||||||
|
T("Copies the configuration plugin ID to the clipboard"),
|
||||||
|
"margin-top: 4px;")
|
||||||
|
])"
|
||||||
|
ShowWarning="@this.IsManagedConfigurationIdMismatch(matchingPlugin, env.ConfigurationId)"
|
||||||
|
WarningText="@T("ID mismatch: the plugin ID differs from the enterprise configuration ID.")"/>
|
||||||
}
|
}
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<EncryptionSecretInfo IsConfigured="@(PluginFactory.EnterpriseEncryption?.IsAvailable is true)"
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
ConfiguredText="@T("Encryption secret: is configured")"
|
||||||
<span>@T("Enterprise configuration ID:") @env.ConfigurationId</span>
|
NotConfiguredText="@T("Encryption secret: is not configured")"/>
|
||||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the config ID to the clipboard")" StringContent=@env.ConfigurationId.ToString()/>
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
|
||||||
<span>@T("Configuration server:") @env.ConfigurationServerUrl</span>
|
|
||||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the server URL to the clipboard")" StringContent=@env.ConfigurationServerUrl/>
|
|
||||||
</div>
|
|
||||||
@if (matchingPlugin is not null)
|
|
||||||
{
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px; margin-top: 4px;">
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
|
||||||
<span>@T("Configuration plugin ID:") @matchingPlugin.Id</span>
|
|
||||||
<MudCopyClipboardButton TooltipMessage="@T("Copies the configuration plugin ID to the clipboard")" StringContent=@matchingPlugin.Id.ToString()/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</MudPaper>
|
|
||||||
}
|
|
||||||
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
|
|
||||||
@if (PluginFactory.EnterpriseEncryption?.IsAvailable is true)
|
|
||||||
{
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small"/>
|
|
||||||
<span>@T("Encryption secret: is configured")</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Error" Size="Size.Small"/>
|
|
||||||
<span>@T("Encryption secret: is not configured")</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</MudText>
|
|
||||||
</MudCollapse>
|
</MudCollapse>
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -319,7 +299,7 @@
|
|||||||
</MudGrid>
|
</MudGrid>
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Verified" HeaderText="License: FSL-1.1-MIT">
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Verified" HeaderText="License: FSL-1.1-MIT">
|
||||||
<MudMarkdown Value="@LICENSE" Props="Markdown.DefaultConfig"/>
|
<MudMarkdown Value="@LICENSE" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||||
</ExpansionPanel>
|
</ExpansionPanel>
|
||||||
</MudExpansionPanels>
|
</MudExpansionPanels>
|
||||||
</InnerScrolling>
|
</InnerScrolling>
|
||||||
|
|||||||
@ -69,7 +69,10 @@ public partial class Information : MSGComponentBase
|
|||||||
|
|
||||||
private bool showDatabaseDetails;
|
private bool showDatabaseDetails;
|
||||||
|
|
||||||
private List<IPluginMetadata> configPlugins = PluginFactory.AvailablePlugins.Where(x => x.Type is PluginType.CONFIGURATION).ToList();
|
private List<IAvailablePlugin> configPlugins = PluginFactory.AvailablePlugins
|
||||||
|
.Where(x => x.Type is PluginType.CONFIGURATION)
|
||||||
|
.OfType<IAvailablePlugin>()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
private sealed record DatabaseDisplayInfo(string Label, string Value);
|
private sealed record DatabaseDisplayInfo(string Label, string Value);
|
||||||
|
|
||||||
@ -77,6 +80,10 @@ public partial class Information : MSGComponentBase
|
|||||||
|
|
||||||
private static bool HasAnyActiveEnvironment => EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Any(e => e.IsActive);
|
private static bool HasAnyActiveEnvironment => EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.Any(e => e.IsActive);
|
||||||
|
|
||||||
|
private bool HasAnyLoadedEnterpriseConfigurationPlugin => EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS
|
||||||
|
.Where(e => e.IsActive)
|
||||||
|
.Any(env => this.FindManagedConfigurationPlugin(env.ConfigurationId) is not null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether the enterprise configuration has details that can be shown/hidden.
|
/// Determines whether the enterprise configuration has details that can be shown/hidden.
|
||||||
/// Returns true if there are details available, false otherwise.
|
/// Returns true if there are details available, false otherwise.
|
||||||
@ -130,7 +137,10 @@ public partial class Information : MSGComponentBase
|
|||||||
switch (triggeredEvent)
|
switch (triggeredEvent)
|
||||||
{
|
{
|
||||||
case Event.PLUGINS_RELOADED:
|
case Event.PLUGINS_RELOADED:
|
||||||
this.configPlugins = PluginFactory.AvailablePlugins.Where(x => x.Type is PluginType.CONFIGURATION).ToList();
|
this.configPlugins = PluginFactory.AvailablePlugins
|
||||||
|
.Where(x => x.Type is PluginType.CONFIGURATION)
|
||||||
|
.OfType<IAvailablePlugin>()
|
||||||
|
.ToList();
|
||||||
await this.InvokeAsync(this.StateHasChanged);
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -196,6 +206,18 @@ public partial class Information : MSGComponentBase
|
|||||||
this.showDatabaseDetails = !this.showDatabaseDetails;
|
this.showDatabaseDetails = !this.showDatabaseDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId)
|
||||||
|
{
|
||||||
|
return this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId == configurationId)
|
||||||
|
// Backward compatibility for already downloaded plugins without ManagedConfigurationId.
|
||||||
|
?? this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId is null && plugin.Id == configurationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsManagedConfigurationIdMismatch(IAvailablePlugin plugin, Guid configurationId)
|
||||||
|
{
|
||||||
|
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CopyStartupLogPath()
|
private async Task CopyStartupLogPath()
|
||||||
{
|
{
|
||||||
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
|
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
|
||||||
|
|||||||
@ -24,6 +24,9 @@ VERSION = "1.0.0"
|
|||||||
-- The type of the plugin:
|
-- The type of the plugin:
|
||||||
TYPE = "CONFIGURATION"
|
TYPE = "CONFIGURATION"
|
||||||
|
|
||||||
|
-- True when this plugin is deployed by an enterprise configuration server:
|
||||||
|
DEPLOYED_USING_CONFIG_SERVER = false
|
||||||
|
|
||||||
-- The authors of the plugin:
|
-- The authors of the plugin:
|
||||||
AUTHORS = {"<Company Name>"}
|
AUTHORS = {"<Company Name>"}
|
||||||
|
|
||||||
@ -163,6 +166,11 @@ CONFIG["SETTINGS"] = {}
|
|||||||
-- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_DOCUMENT_ANALYSIS_2025.
|
-- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_DOCUMENT_ANALYSIS_2025.
|
||||||
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_DOCUMENT_ANALYSIS_2025" }
|
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_DOCUMENT_ANALYSIS_2025" }
|
||||||
|
|
||||||
|
-- Configure the preselected provider.
|
||||||
|
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
|
||||||
|
-- Please note: using an empty string ("") will lock the preselected provider selection, even though no valid preselected provider is found.
|
||||||
|
-- CONFIG["SETTINGS"]["DataApp.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000"
|
||||||
|
|
||||||
-- Configure the preselected profile.
|
-- Configure the preselected profile.
|
||||||
-- It must be one of the profile IDs defined in CONFIG["PROFILES"].
|
-- It must be one of the profile IDs defined in CONFIG["PROFILES"].
|
||||||
-- Please note: using an empty string ("") will lock the preselected profile selection, even though no valid preselected profile is found.
|
-- Please note: using an empty string ("") will lock the preselected profile selection, even though no valid preselected profile is found.
|
||||||
|
|||||||
@ -2175,9 +2175,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"]
|
|||||||
-- Administration settings are not visible
|
-- Administration settings are not visible
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Die Optionen für die Administration sind nicht sichtbar."
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Die Optionen für die Administration sind nicht sichtbar."
|
||||||
|
|
||||||
|
-- Embedding Result
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Einbettungsergebnis"
|
||||||
|
|
||||||
-- Delete
|
-- Delete
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Löschen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Löschen"
|
||||||
|
|
||||||
|
-- Embed text
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1644934561"] = "Text einbetten"
|
||||||
|
|
||||||
|
-- Test Embedding Provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Anbieter für Einbettung testen"
|
||||||
|
|
||||||
-- Add Embedding
|
-- Add Embedding
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Einbettung hinzufügen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Einbettung hinzufügen"
|
||||||
|
|
||||||
@ -2190,6 +2199,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253
|
|||||||
-- Add Embedding Provider
|
-- Add Embedding Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Einbettungsanbieter hinzufügen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Einbettungsanbieter hinzufügen"
|
||||||
|
|
||||||
|
-- Add text that should be embedded:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Text zum Einbetten eingeben:"
|
||||||
|
|
||||||
|
-- Embedding Vector (one dimension per line)
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Einbettungsvektor (eine Dimension pro Zeile)"
|
||||||
|
|
||||||
-- Model
|
-- Model
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Modell"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Modell"
|
||||||
|
|
||||||
@ -2199,6 +2214,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199
|
|||||||
-- Name
|
-- Name
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name"
|
||||||
|
|
||||||
|
-- No embedding was returned.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "Es wurde keine Einbettung zurückgegeben."
|
||||||
|
|
||||||
-- Configured Embedding Providers
|
-- Configured Embedding Providers
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Konfigurierte Anbieter für Einbettungen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Konfigurierte Anbieter für Einbettungen"
|
||||||
|
|
||||||
@ -2208,6 +2226,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512
|
|||||||
-- Edit
|
-- Edit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Bearbeiten"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Bearbeiten"
|
||||||
|
|
||||||
|
-- Close
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Schließen"
|
||||||
|
|
||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Aktionen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Aktionen"
|
||||||
|
|
||||||
@ -2229,6 +2250,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130
|
|||||||
-- Open Dashboard
|
-- Open Dashboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Dashboard öffnen"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Dashboard öffnen"
|
||||||
|
|
||||||
|
-- Test
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Testen"
|
||||||
|
|
||||||
|
-- Example text to embed
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Beispieltext zum Einbetten"
|
||||||
|
|
||||||
-- Provider
|
-- Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Anbieter"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Anbieter"
|
||||||
|
|
||||||
@ -3333,6 +3360,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Anb
|
|||||||
-- Cancel
|
-- Cancel
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Abbrechen"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Abbrechen"
|
||||||
|
|
||||||
|
-- Embedding Vector
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Einbettungsvektor"
|
||||||
|
|
||||||
|
-- Close
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Schließen"
|
||||||
|
|
||||||
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
|
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Leider ist die GPL-Lizenz von Pandoc nicht mit der Lizenz von AI Studio kompatibel. Software unter der GPL-Lizenz ist jedoch kostenlos und frei nutzbar. Sie müssen die GPL-Lizenz akzeptieren, bevor wir Pandoc automatisch für Sie herunterladen und installieren können (empfohlen). Alternativ können Sie Pandoc auch selbst herunterladen – entweder mit den untenstehenden Anweisungen oder auf anderem Weg, zum Beispiel über den Paketmanager Ihres Betriebssystems."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Leider ist die GPL-Lizenz von Pandoc nicht mit der Lizenz von AI Studio kompatibel. Software unter der GPL-Lizenz ist jedoch kostenlos und frei nutzbar. Sie müssen die GPL-Lizenz akzeptieren, bevor wir Pandoc automatisch für Sie herunterladen und installieren können (empfohlen). Alternativ können Sie Pandoc auch selbst herunterladen – entweder mit den untenstehenden Anweisungen oder auf anderem Weg, zum Beispiel über den Paketmanager Ihres Betriebssystems."
|
||||||
|
|
||||||
|
|||||||
@ -2175,9 +2175,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T922066419"]
|
|||||||
-- Administration settings are not visible
|
-- Administration settings are not visible
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Administration settings are not visible"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T929143445"] = "Administration settings are not visible"
|
||||||
|
|
||||||
|
-- Embedding Result
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result"
|
||||||
|
|
||||||
-- Delete
|
-- Delete
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete"
|
||||||
|
|
||||||
|
-- Embed text
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1644934561"] = "Embed text"
|
||||||
|
|
||||||
|
-- Test Embedding Provider
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Test Embedding Provider"
|
||||||
|
|
||||||
-- Add Embedding
|
-- Add Embedding
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding"
|
||||||
|
|
||||||
@ -2190,6 +2199,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253
|
|||||||
-- Add Embedding Provider
|
-- Add Embedding Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider"
|
||||||
|
|
||||||
|
-- Add text that should be embedded:
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1992646324"] = "Add text that should be embedded:"
|
||||||
|
|
||||||
|
-- Embedding Vector (one dimension per line)
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Embedding Vector (one dimension per line)"
|
||||||
|
|
||||||
-- Model
|
-- Model
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model"
|
||||||
|
|
||||||
@ -2199,6 +2214,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199
|
|||||||
-- Name
|
-- Name
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name"
|
||||||
|
|
||||||
|
-- No embedding was returned.
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "No embedding was returned."
|
||||||
|
|
||||||
-- Configured Embedding Providers
|
-- Configured Embedding Providers
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers"
|
||||||
|
|
||||||
@ -2208,6 +2226,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512
|
|||||||
-- Edit
|
-- Edit
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit"
|
||||||
|
|
||||||
|
-- Close
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Close"
|
||||||
|
|
||||||
-- Actions
|
-- Actions
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions"
|
||||||
|
|
||||||
@ -2229,6 +2250,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130
|
|||||||
-- Open Dashboard
|
-- Open Dashboard
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard"
|
||||||
|
|
||||||
|
-- Test
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Test"
|
||||||
|
|
||||||
|
-- Example text to embed
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T816748904"] = "Example text to embed"
|
||||||
|
|
||||||
-- Provider
|
-- Provider
|
||||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider"
|
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider"
|
||||||
|
|
||||||
@ -3333,6 +3360,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Pro
|
|||||||
-- Cancel
|
-- Cancel
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel"
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel"
|
||||||
|
|
||||||
|
-- Embedding Vector
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embedding Vector"
|
||||||
|
|
||||||
|
-- Close
|
||||||
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close"
|
||||||
|
|
||||||
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
|
-- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system.
|
||||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system."
|
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system."
|
||||||
|
|
||||||
|
|||||||
@ -87,6 +87,13 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
|||||||
return Task.FromResult(string.Empty);
|
return Task.FromResult(string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
|
||||||
|
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -114,6 +114,12 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
|
|||||||
return Task.FromResult(string.Empty);
|
return Task.FromResult(string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
@ -98,6 +99,9 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default);
|
public abstract Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
public abstract Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default);
|
||||||
|
|
||||||
@ -645,6 +649,81 @@ public abstract class BaseProvider : IProvider, ISecretId
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async Task<IReadOnlyList<IReadOnlyList<float>>> PerformStandardTextEmbeddingRequest(RequestedSecret requestedSecret, Model embeddingModel, Host host = Host.NONE, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Add the model name to the form data. Ensure that a model name is always provided.
|
||||||
|
// Otherwise, the StringContent constructor will throw an exception.
|
||||||
|
//
|
||||||
|
var modelName = embeddingModel.Id;
|
||||||
|
if (string.IsNullOrWhiteSpace(modelName))
|
||||||
|
modelName = "placeholder";
|
||||||
|
|
||||||
|
// Prepare the HTTP embedding request:
|
||||||
|
var payload = new
|
||||||
|
{
|
||||||
|
model = modelName,
|
||||||
|
input = texts,
|
||||||
|
encoding_format = "float"
|
||||||
|
};
|
||||||
|
|
||||||
|
var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS);
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, host.EmbeddingURL());
|
||||||
|
|
||||||
|
// Handle the authorization header based on the provider:
|
||||||
|
switch (this.Provider)
|
||||||
|
{
|
||||||
|
case LLMProviders.SELF_HOSTED:
|
||||||
|
if(requestedSecret.Success)
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if(!requestedSecret.Success)
|
||||||
|
{
|
||||||
|
this.logger.LogError("No valid API key available for embedding request.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the content:
|
||||||
|
request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json");
|
||||||
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
|
var responseBody = response.Content.ReadAsStringAsync(token).Result;
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
this.logger.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var embeddingResponse = JsonSerializer.Deserialize<EmbeddingResponse>(responseBody, JSON_SERIALIZER_OPTIONS);
|
||||||
|
if (embeddingResponse is { Data: not null })
|
||||||
|
{
|
||||||
|
return embeddingResponse.Data
|
||||||
|
.Select(d => d.Embedding?.ToArray() ?? [])
|
||||||
|
.Cast<IReadOnlyList<float>>()
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.logger.LogError("Was not able to deserialize the embedding response.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
this.logger.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse and convert API parameters from a provided JSON string into a dictionary,
|
/// Parse and convert API parameters from a provided JSON string into a dictionary,
|
||||||
/// optionally merging additional parameters and removing specific keys.
|
/// optionally merging additional parameters and removing specific keys.
|
||||||
|
|||||||
@ -87,6 +87,12 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "h
|
|||||||
return Task.FromResult(string.Empty);
|
return Task.FromResult(string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
12
app/MindWork AI Studio/Provider/EmbeddingData.cs
Normal file
12
app/MindWork AI Studio/Provider/EmbeddingData.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// ReSharper disable CollectionNeverUpdated.Global
|
||||||
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
|
// ReSharper disable once ClassNeverInstantiated.Global
|
||||||
|
public sealed record EmbeddingData
|
||||||
|
{
|
||||||
|
public string? Object { get; set; }
|
||||||
|
|
||||||
|
public List<float>? Embedding { get; set; }
|
||||||
|
|
||||||
|
public int? Index { get; set; }
|
||||||
|
}
|
||||||
14
app/MindWork AI Studio/Provider/EmbeddingResponse.cs
Normal file
14
app/MindWork AI Studio/Provider/EmbeddingResponse.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
|
public sealed record EmbeddingResponse
|
||||||
|
{
|
||||||
|
public string? Id { get; init; }
|
||||||
|
|
||||||
|
public string? Object { get; init; }
|
||||||
|
|
||||||
|
public List<EmbeddingData>? Data { get; init; }
|
||||||
|
|
||||||
|
public string? Model { get; init; }
|
||||||
|
|
||||||
|
public EmbeddingUsage? Usage { get; init; }
|
||||||
|
}
|
||||||
11
app/MindWork AI Studio/Provider/EmbeddingUsage.cs
Normal file
11
app/MindWork AI Studio/Provider/EmbeddingUsage.cs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// ReSharper disable ClassNeverInstantiated.Global
|
||||||
|
namespace AIStudio.Provider;
|
||||||
|
|
||||||
|
public sealed record EmbeddingUsage
|
||||||
|
{
|
||||||
|
public int? PromptTokens { get; set; }
|
||||||
|
|
||||||
|
public int? TotalTokens { get; set; }
|
||||||
|
|
||||||
|
public int? CompletionTokens { get; set; }
|
||||||
|
}
|
||||||
@ -89,6 +89,12 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https:/
|
|||||||
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -88,6 +88,12 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://ch
|
|||||||
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
namespace AIStudio.Provider.Google;
|
||||||
|
|
||||||
|
public sealed record GoogleEmbedding
|
||||||
|
{
|
||||||
|
public List<float>? Values { get; init; }
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace AIStudio.Provider.Google;
|
||||||
|
|
||||||
|
public sealed record GoogleEmbeddingResponse
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(GoogleEmbeddingListConverter))]
|
||||||
|
public List<GoogleEmbedding>? Embedding { get; init; }
|
||||||
|
|
||||||
|
private sealed class GoogleEmbeddingListConverter : JsonConverter<List<GoogleEmbedding>>
|
||||||
|
{
|
||||||
|
public override List<GoogleEmbedding> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.StartObject)
|
||||||
|
{
|
||||||
|
var single = JsonSerializer.Deserialize<GoogleEmbedding>(ref reader, options);
|
||||||
|
return single is null ? new() : new() { single };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.StartArray)
|
||||||
|
return JsonSerializer.Deserialize<List<GoogleEmbedding>>(ref reader, options) ?? new();
|
||||||
|
|
||||||
|
throw new JsonException("Expected object or array for embedding.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, List<GoogleEmbedding> value, JsonSerializerOptions options) =>
|
||||||
|
JsonSerializer.Serialize(writer, value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +0,0 @@
|
|||||||
namespace AIStudio.Provider.Google;
|
|
||||||
|
|
||||||
public readonly record struct Model(string Name, string DisplayName);
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
namespace AIStudio.Provider.Google;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A data model for the response from the model endpoint.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="Models"></param>
|
|
||||||
public readonly record struct ModelsResponse(IList<Model> Models);
|
|
||||||
@ -22,7 +22,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
|||||||
public override string InstanceName { get; set; } = "Google Gemini";
|
public override string InstanceName { get; set; } = "Google Gemini";
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||||
{
|
{
|
||||||
// Get the API key:
|
// Get the API key:
|
||||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.LLM_PROVIDER);
|
||||||
@ -76,57 +76,122 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
|||||||
|
|
||||||
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
|
public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default)
|
||||||
{
|
{
|
||||||
yield break;
|
yield break;
|
||||||
}
|
}
|
||||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<string> TranscribeAudioAsync(Provider.Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return Task.FromResult(string.Empty);
|
return Task.FromResult(string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inhertidoc />
|
||||||
public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
{
|
{
|
||||||
var modelResponse = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
|
||||||
if(modelResponse == default)
|
try
|
||||||
|
{
|
||||||
|
var modelName = embeddingModel.Id;
|
||||||
|
if (string.IsNullOrWhiteSpace(modelName))
|
||||||
|
{
|
||||||
|
LOGGER.LogError("No model name provided for embedding request.");
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
return modelResponse.Models.Where(model =>
|
|
||||||
model.Name.StartsWith("models/gemini-", StringComparison.OrdinalIgnoreCase) && !model.Name.Contains("embed"))
|
|
||||||
.Select(n => new Provider.Model(n.Name.Replace("models/", string.Empty), n.DisplayName));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
if (modelName.StartsWith("models/", StringComparison.OrdinalIgnoreCase))
|
||||||
public override Task<IEnumerable<Provider.Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
modelName = modelName.Substring("models/".Length);
|
||||||
{
|
|
||||||
return Task.FromResult(Enumerable.Empty<Provider.Model>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<IEnumerable<Provider.Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
if (!requestedSecret.Success)
|
||||||
{
|
{
|
||||||
var modelResponse = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional);
|
LOGGER.LogError("No valid API key available for embedding request.");
|
||||||
if(modelResponse == default)
|
|
||||||
return [];
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return modelResponse.Models.Where(model =>
|
// Prepare the Google Gemini embedding request:
|
||||||
model.Name.StartsWith("models/text-embedding-", StringComparison.OrdinalIgnoreCase) ||
|
var payload = new
|
||||||
model.Name.StartsWith("models/gemini-embed", StringComparison.OrdinalIgnoreCase))
|
{
|
||||||
.Select(n => new Provider.Model(n.Name.Replace("models/", string.Empty), n.DisplayName));
|
content = new
|
||||||
|
{
|
||||||
|
parts = texts.Select(text => new { text }).ToArray()
|
||||||
|
},
|
||||||
|
|
||||||
|
taskType = "SEMANTIC_SIMILARITY"
|
||||||
|
};
|
||||||
|
|
||||||
|
var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS);
|
||||||
|
var embedUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{modelName}:embedContent";
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, embedUrl);
|
||||||
|
request.Headers.Add("x-goog-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||||
|
|
||||||
|
// Set the content:
|
||||||
|
request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
LOGGER.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var embeddingResponse = JsonSerializer.Deserialize<GoogleEmbeddingResponse>(responseBody, JSON_SERIALIZER_OPTIONS);
|
||||||
|
if (embeddingResponse is { Embedding: not null })
|
||||||
|
{
|
||||||
|
return embeddingResponse.Embedding
|
||||||
|
.Select(d => d.Values?.ToArray() ?? [])
|
||||||
|
.Cast<IReadOnlyList<float>>()
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOGGER.LogError("Was not able to deserialize the embedding response.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LOGGER.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<IEnumerable<Provider.Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return Task.FromResult(Enumerable.Empty<Provider.Model>());
|
var models = await this.LoadModels(SecretStoreType.LLM_PROVIDER, token, apiKeyProvisional);
|
||||||
|
return models.Where(model =>
|
||||||
|
model.Id.StartsWith("gemini-", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!this.IsEmbeddingModel(model.Id))
|
||||||
|
.Select(this.WithDisplayNameFallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<IEnumerable<Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Enumerable.Empty<Model>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<IEnumerable<Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
var models = await this.LoadModels(SecretStoreType.EMBEDDING_PROVIDER, token, apiKeyProvisional);
|
||||||
|
return models.Where(model => this.IsEmbeddingModel(model.Id))
|
||||||
|
.Select(this.WithDisplayNameFallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task<IEnumerable<Model>> GetTranscriptionModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Enumerable.Empty<Model>());
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private async Task<ModelsResponse> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
private async Task<IReadOnlyList<Model>> LoadModels(SecretStoreType storeType, CancellationToken token, string? apiKeyProvisional = null)
|
||||||
{
|
{
|
||||||
var secretKey = apiKeyProvisional switch
|
var secretKey = apiKeyProvisional switch
|
||||||
{
|
{
|
||||||
@ -138,16 +203,57 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (secretKey is null)
|
if (string.IsNullOrWhiteSpace(secretKey))
|
||||||
return default;
|
return [];
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||||
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, $"models?key={secretKey}");
|
|
||||||
using var response = await this.httpClient.SendAsync(request, token);
|
using var response = await this.httpClient.SendAsync(request, token);
|
||||||
|
|
||||||
if(!response.IsSuccessStatusCode)
|
if(!response.IsSuccessStatusCode)
|
||||||
return default;
|
{
|
||||||
|
LOGGER.LogError("Failed to load models with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, await response.Content.ReadAsStringAsync(token));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
var modelResponse = await response.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||||
return modelResponse;
|
if (modelResponse == default || modelResponse.Data.Count is 0)
|
||||||
|
{
|
||||||
|
LOGGER.LogError("Google model list response did not contain a valid data array.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return modelResponse.Data
|
||||||
|
.Where(model => !string.IsNullOrWhiteSpace(model.Id))
|
||||||
|
.Select(model => new Model(this.NormalizeModelId(model.Id), model.DisplayName))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LOGGER.LogError("Failed to parse Google model list response: '{Message}'.", e.Message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsEmbeddingModel(string modelId)
|
||||||
|
{
|
||||||
|
return modelId.Contains("embedding", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
modelId.Contains("embed", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Model WithDisplayNameFallback(Model model)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(model.DisplayName)
|
||||||
|
? new Model(model.Id, model.Id)
|
||||||
|
: model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string NormalizeModelId(string modelId)
|
||||||
|
{
|
||||||
|
return modelId.StartsWith("models/", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? modelId["models/".Length..]
|
||||||
|
: modelId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,6 +88,12 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq.
|
|||||||
return Task.FromResult(string.Empty);
|
return Task.FromResult(string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -87,6 +87,13 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
|
|||||||
return Task.FromResult(string.Empty);
|
return Task.FromResult(string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
|
||||||
|
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -92,6 +92,12 @@ public sealed class ProviderHuggingFace : BaseProvider
|
|||||||
return Task.FromResult(string.Empty);
|
return Task.FromResult(string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -60,6 +60,16 @@ public interface IProvider
|
|||||||
/// <returns>>The transcription result.</returns>
|
/// <returns>>The transcription result.</returns>
|
||||||
public Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default);
|
public Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Embed a text file.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="embeddingModel">The model to use for embedding.</param>
|
||||||
|
/// <param name="settingsManager">The settings manager instance to use.</param>
|
||||||
|
/// <param name="token">The cancellation token.</param>
|
||||||
|
/// /// <param name="texts">A single string or a list of strings to embed.</param>
|
||||||
|
/// <returns>>The embedded text as a single vector or as a list of vectors.</returns>
|
||||||
|
public Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Load all possible text models that can be used with this provider.
|
/// Load all possible text models that can be used with this provider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -89,6 +89,13 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
|
|||||||
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
|
||||||
|
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -40,6 +40,8 @@ public class NoProvider : IProvider
|
|||||||
|
|
||||||
public Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) => Task.FromResult(string.Empty);
|
public Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) => Task.FromResult(string.Empty);
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts) => Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||||
|
|
||||||
public IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => [ Capability.NONE ];
|
public IReadOnlyCollection<Capability> GetModelCapabilities(Model model) => [ Capability.NONE ];
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -225,6 +225,13 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
|
|||||||
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
|
||||||
|
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -95,6 +95,13 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
|
|||||||
return Task.FromResult(string.Empty);
|
return Task.FromResult(string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER);
|
||||||
|
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -95,6 +95,12 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
|
|||||||
return Task.FromResult(string.Empty);
|
return Task.FromResult(string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -30,6 +30,11 @@ public static class HostExtensions
|
|||||||
_ => "audio/transcriptions",
|
_ => "audio/transcriptions",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static string EmbeddingURL(this Host host) => host switch
|
||||||
|
{
|
||||||
|
_ => "embeddings",
|
||||||
|
};
|
||||||
|
|
||||||
public static bool IsChatSupported(this Host host)
|
public static bool IsChatSupported(this Host host)
|
||||||
{
|
{
|
||||||
switch (host)
|
switch (host)
|
||||||
|
|||||||
@ -95,6 +95,13 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
|||||||
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, host, token);
|
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, host, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override async Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER, isTrying: true);
|
||||||
|
return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, host, token: token, texts: texts);
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@ -88,6 +88,12 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai
|
|||||||
return Task.FromResult(string.Empty);
|
return Task.FromResult(string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inhertidoc />
|
||||||
|
public override Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts)
|
||||||
|
{
|
||||||
|
return Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
public override async Task<IEnumerable<Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -28,14 +28,14 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
|
|||||||
private Expression<Func<TClass, TValue>> PropertyExpression { get; }
|
private Expression<Func<TClass, TValue>> PropertyExpression { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates whether the configuration is managed by a plugin and is therefore locked.
|
/// Indicates whether the configuration is locked by a configuration plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsLocked { get; private set; }
|
public bool IsLocked { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The ID of the plugin that manages this configuration. This is set when the configuration is locked.
|
/// The ID of the plugin that locked this configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Guid MangedByConfigPluginId { get; private set; }
|
public Guid LockedByConfigPluginId { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default value for the configuration property. This is used when resetting the property to its default state.
|
/// The default value for the configuration property. This is used when resetting the property to its default state.
|
||||||
@ -43,30 +43,74 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
|
|||||||
public required TValue Default { get; init; }
|
public required TValue Default { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Locks the configuration state, indicating that it is managed by a specific plugin.
|
/// Indicates whether a plugin contribution is available.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pluginId">The ID of the plugin that is managing this configuration.</param>
|
public bool HasPluginContribution { get; private set; }
|
||||||
public void LockManagedState(Guid pluginId)
|
|
||||||
|
/// <summary>
|
||||||
|
/// The additive value contribution provided by a configuration plugin.
|
||||||
|
/// </summary>
|
||||||
|
public TValue PluginContribution { get; private set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the plugin that provided the additive value contribution.
|
||||||
|
/// </summary>
|
||||||
|
public Guid PluginContributionByConfigPluginId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Locks the configuration state, indicating that it is controlled by a specific plugin.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pluginId">The ID of the plugin that is locking this configuration.</param>
|
||||||
|
public void LockConfiguration(Guid pluginId)
|
||||||
{
|
{
|
||||||
this.IsLocked = true;
|
this.IsLocked = true;
|
||||||
this.MangedByConfigPluginId = pluginId;
|
this.LockedByConfigPluginId = pluginId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets the managed state of the configuration, allowing it to be modified again.
|
/// Resets the locked state of the configuration, allowing it to be modified again.
|
||||||
/// This will also reset the property to its default value.
|
/// This will also reset the property to its default value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ResetManagedState()
|
public void ResetLockedConfiguration()
|
||||||
{
|
{
|
||||||
this.IsLocked = false;
|
this.IsLocked = false;
|
||||||
this.MangedByConfigPluginId = Guid.Empty;
|
this.LockedByConfigPluginId = Guid.Empty;
|
||||||
this.Reset();
|
this.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unlocks the configuration state without changing the current value.
|
||||||
|
/// </summary>
|
||||||
|
public void UnlockConfiguration()
|
||||||
|
{
|
||||||
|
this.IsLocked = false;
|
||||||
|
this.LockedByConfigPluginId = Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores an additive plugin contribution.
|
||||||
|
/// </summary>
|
||||||
|
public void SetPluginContribution(TValue value, Guid pluginId)
|
||||||
|
{
|
||||||
|
this.PluginContribution = value;
|
||||||
|
this.PluginContributionByConfigPluginId = pluginId;
|
||||||
|
this.HasPluginContribution = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the additive plugin contribution without changing the current value.
|
||||||
|
/// </summary>
|
||||||
|
public void ClearPluginContribution()
|
||||||
|
{
|
||||||
|
this.PluginContribution = default!;
|
||||||
|
this.PluginContributionByConfigPluginId = Guid.Empty;
|
||||||
|
this.HasPluginContribution = false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resets the configuration property to its default value.
|
/// Resets the configuration property to its default value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Reset()
|
private void Reset()
|
||||||
{
|
{
|
||||||
var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData);
|
var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData);
|
||||||
var memberExpression = this.PropertyExpression.GetMemberExpression();
|
var memberExpression = this.PropertyExpression.GetMemberExpression();
|
||||||
|
|||||||
@ -201,7 +201,7 @@ public static class ConfigurationSelectDataFactory
|
|||||||
public static IEnumerable<ConfigurationSelectData<string>> GetProfilesData(IEnumerable<Profile> profiles)
|
public static IEnumerable<ConfigurationSelectData<string>> GetProfilesData(IEnumerable<Profile> profiles)
|
||||||
{
|
{
|
||||||
foreach (var profile in profiles.GetAllProfiles())
|
foreach (var profile in profiles.GetAllProfiles())
|
||||||
yield return new(profile.Name, profile.Id);
|
yield return new(profile.GetSafeName(), profile.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<ConfigurationSelectData<string>> GetTranscriptionProvidersData(IEnumerable<TranscriptionProvider> transcriptionProviders)
|
public static IEnumerable<ConfigurationSelectData<string>> GetTranscriptionProvidersData(IEnumerable<TranscriptionProvider> transcriptionProviders)
|
||||||
|
|||||||
@ -65,7 +65,7 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should we preselect a provider for the entire app?
|
/// Should we preselect a provider for the entire app?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string PreselectedProvider { get; set; } = string.Empty;
|
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProvider, string.Empty);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should we preselect a profile for the entire app?
|
/// Should we preselect a profile for the entire app?
|
||||||
|
|||||||
@ -56,43 +56,43 @@ public sealed record EmbeddingProvider(
|
|||||||
provider = NONE;
|
provider = NONE;
|
||||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid ID. The ID must be a valid GUID.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name))
|
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid name.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid name. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
|
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid LLM provider enum value.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid LLM provider enum value. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
|
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid host enum value.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid host enum value. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
|
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid hostname.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
|
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model table.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model table. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadModelTable(idx, modelTable, out var model))
|
if (!TryReadModelTable(idx, modelTable, configPluginId, out var model))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model configuration.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model configuration. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ public sealed record EmbeddingProvider(
|
|||||||
if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
|
if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
|
||||||
{
|
{
|
||||||
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
|
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var encryption = PluginFactory.EnterpriseEncryption;
|
var encryption = PluginFactory.EnterpriseEncryption;
|
||||||
@ -128,31 +128,31 @@ public sealed record EmbeddingProvider(
|
|||||||
name,
|
name,
|
||||||
decryptedApiKey,
|
decryptedApiKey,
|
||||||
SecretStoreType.EMBEDDING_PROVIDER));
|
SecretStoreType.EMBEDDING_PROVIDER));
|
||||||
LOGGER.LogDebug($"Successfully decrypted API key for embedding provider {idx}. It will be stored in the OS keyring.");
|
LOGGER.LogDebug($"Successfully decrypted API key for embedding provider {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
LOGGER.LogWarning($"Failed to decrypt API key for embedding provider {idx}. The encryption secret may be incorrect.");
|
LOGGER.LogWarning($"Failed to decrypt API key for embedding provider {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} contains an encrypted API key, but no encryption secret is configured.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} contains an encrypted API key, but no encryption secret is configured. (Plugin ID: {configPluginId})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadModelTable(int idx, LuaTable table, out Model model)
|
private static bool TryReadModelTable(int idx, LuaTable table, Guid configPluginId, out Model model)
|
||||||
{
|
{
|
||||||
model = default;
|
model = default;
|
||||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
|
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model ID.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model ID. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
|
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model display name.");
|
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid model display name. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -582,6 +582,90 @@ public static partial class ManagedConfiguration
|
|||||||
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
return HandleParsedValue(configPluginId, dryRun, successful, configMeta, configuredValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to process additive plugin contributions for enum set settings from a Lua table.
|
||||||
|
/// The contributed values are merged into the existing set, and the setting remains unlocked
|
||||||
|
/// so users can add additional values.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="configPluginId">The ID of the related configuration plugin.</param>
|
||||||
|
/// <param name="settings">The Lua table containing the settings to process.</param>
|
||||||
|
/// <param name="configSelection">The expression to select the configuration class.</param>
|
||||||
|
/// <param name="propertyExpression">The expression to select the property within the configuration class.</param>
|
||||||
|
/// <param name="dryRun">When true, the method will not apply any changes but only check if the configuration can be read.</param>
|
||||||
|
/// <typeparam name="TClass">The type of the configuration class.</typeparam>
|
||||||
|
/// <typeparam name="TValue">The type of the property within the configuration class. It is also the type of the set
|
||||||
|
/// elements, which must be an enum.</typeparam>
|
||||||
|
/// <returns>True when the configuration was successfully processed, otherwise false.</returns>
|
||||||
|
public static bool TryProcessConfigurationWithPluginContribution<TClass, TValue>(
|
||||||
|
Expression<Func<Data, TClass>> configSelection,
|
||||||
|
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
||||||
|
Guid configPluginId,
|
||||||
|
LuaTable settings,
|
||||||
|
bool dryRun)
|
||||||
|
where TValue : Enum
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Handle configured enum sets (additive merge)
|
||||||
|
//
|
||||||
|
|
||||||
|
// Check if that configuration was registered:
|
||||||
|
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var successful = false;
|
||||||
|
var configuredValue = new HashSet<TValue>();
|
||||||
|
|
||||||
|
// Step 1 -- try to read the Lua value (we expect a table) out of the Lua table:
|
||||||
|
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
|
||||||
|
configuredLuaList.Type is LuaValueType.Table &&
|
||||||
|
configuredLuaList.TryRead<LuaTable>(out var valueTable))
|
||||||
|
{
|
||||||
|
// Determine the length of the Lua table and prepare a set to hold the parsed values:
|
||||||
|
var len = valueTable.ArrayLength;
|
||||||
|
var set = new HashSet<TValue>(len);
|
||||||
|
|
||||||
|
// Iterate over each entry in the Lua table:
|
||||||
|
for (var index = 1; index <= len; index++)
|
||||||
|
{
|
||||||
|
// Retrieve the Lua value at the current index:
|
||||||
|
var value = valueTable[index];
|
||||||
|
|
||||||
|
// Step 2 -- try to read the Lua value as a string:
|
||||||
|
if (value.Type is LuaValueType.String && value.TryRead<string>(out var configuredLuaValueText))
|
||||||
|
{
|
||||||
|
// Step 3 -- try to parse the string as the target type:
|
||||||
|
if (Enum.TryParse(typeof(TValue), configuredLuaValueText, true, out var configuredEnum))
|
||||||
|
set.Add((TValue)configuredEnum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configuredValue = set;
|
||||||
|
successful = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dryRun)
|
||||||
|
return successful;
|
||||||
|
|
||||||
|
if (successful)
|
||||||
|
{
|
||||||
|
var configInstance = configSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData);
|
||||||
|
var currentValue = propertyExpression.Compile().Invoke(configInstance);
|
||||||
|
var merged = new HashSet<TValue>(currentValue);
|
||||||
|
merged.UnionWith(configuredValue);
|
||||||
|
configMeta.SetValue(merged);
|
||||||
|
configMeta.SetPluginContribution(new HashSet<TValue>(configuredValue), configPluginId);
|
||||||
|
}
|
||||||
|
else if (configMeta.HasPluginContribution && configMeta.PluginContributionByConfigPluginId == configPluginId)
|
||||||
|
{
|
||||||
|
configMeta.ClearPluginContribution();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId)
|
||||||
|
configMeta.UnlockConfiguration();
|
||||||
|
|
||||||
|
return successful;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to process the configuration settings from a Lua table for string set types.
|
/// Attempts to process the configuration settings from a Lua table for string set types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -744,12 +828,12 @@ public static partial class ManagedConfiguration
|
|||||||
// Case: the setting was configured, and we could read the value successfully.
|
// Case: the setting was configured, and we could read the value successfully.
|
||||||
//
|
//
|
||||||
|
|
||||||
// Set the configured value and lock the managed state:
|
// Set the configured value and lock the configuration:
|
||||||
configMeta.SetValue(configuredValue);
|
configMeta.SetValue(configuredValue);
|
||||||
configMeta.LockManagedState(configPluginId);
|
configMeta.LockConfiguration(configPluginId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == configPluginId:
|
case false when configMeta.IsLocked && configMeta.LockedByConfigPluginId == configPluginId:
|
||||||
//
|
//
|
||||||
// Case: the setting was configured previously, but we could not read the value successfully.
|
// Case: the setting was configured previously, but we could not read the value successfully.
|
||||||
// This happens when the setting was removed from the configuration plugin. We handle that
|
// This happens when the setting was removed from the configuration plugin. We handle that
|
||||||
@ -757,10 +841,10 @@ public static partial class ManagedConfiguration
|
|||||||
//
|
//
|
||||||
// The other case, when the setting was locked and managed by a different configuration plugin,
|
// The other case, when the setting was locked and managed by a different configuration plugin,
|
||||||
// is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin
|
// is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin
|
||||||
// is still available. If it is not available, it resets the managed state of the
|
// is still available. If it is not available, it resets the locked state of the
|
||||||
// configuration setting, allowing it to be reconfigured by a different plugin or left unchanged.
|
// configuration setting, allowing it to be reconfigured by a different plugin or left unchanged.
|
||||||
//
|
//
|
||||||
configMeta.ResetManagedState();
|
configMeta.ResetLockedConfiguration();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case false:
|
case false:
|
||||||
|
|||||||
@ -9,6 +9,7 @@ namespace AIStudio.Settings;
|
|||||||
public static partial class ManagedConfiguration
|
public static partial class ManagedConfiguration
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<string, IConfig> METADATA = new();
|
private static readonly ConcurrentDictionary<string, IConfig> METADATA = new();
|
||||||
|
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to retrieve the configuration metadata for a given configuration selection and
|
/// Attempts to retrieve the configuration metadata for a given configuration selection and
|
||||||
@ -251,13 +252,13 @@ public static partial class ManagedConfiguration
|
|||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
||||||
if (plugin is null)
|
if (plugin is null)
|
||||||
{
|
{
|
||||||
configMeta.ResetManagedState();
|
configMeta.ResetLockedConfiguration();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,13 +273,13 @@ public static partial class ManagedConfiguration
|
|||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
||||||
if (plugin is null)
|
if (plugin is null)
|
||||||
{
|
{
|
||||||
configMeta.ResetManagedState();
|
configMeta.ResetLockedConfiguration();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -296,13 +297,13 @@ public static partial class ManagedConfiguration
|
|||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
||||||
if (plugin is null)
|
if (plugin is null)
|
||||||
{
|
{
|
||||||
configMeta.ResetManagedState();
|
configMeta.ResetLockedConfiguration();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,13 +320,13 @@ public static partial class ManagedConfiguration
|
|||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
||||||
if (plugin is null)
|
if (plugin is null)
|
||||||
{
|
{
|
||||||
configMeta.ResetManagedState();
|
configMeta.ResetLockedConfiguration();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,13 +341,38 @@ public static partial class ManagedConfiguration
|
|||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
||||||
if (plugin is null)
|
if (plugin is null)
|
||||||
{
|
{
|
||||||
configMeta.ResetManagedState();
|
configMeta.ResetLockedConfiguration();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a plugin contribution is left over from a configuration plugin that is no longer available.
|
||||||
|
/// If so, it clears the contribution and returns true.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsPluginContributionLeftOver<TClass, TValue>(
|
||||||
|
Expression<Func<Data, TClass>> configSelection,
|
||||||
|
Expression<Func<TClass, ISet<TValue>>> propertyExpression,
|
||||||
|
IEnumerable<IAvailablePlugin> availablePlugins)
|
||||||
|
{
|
||||||
|
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!configMeta.HasPluginContribution || configMeta.PluginContributionByConfigPluginId == Guid.Empty)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.PluginContributionByConfigPluginId);
|
||||||
|
if (plugin is null)
|
||||||
|
{
|
||||||
|
configMeta.ClearPluginContribution();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,13 +387,13 @@ public static partial class ManagedConfiguration
|
|||||||
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
if (!TryGet(configSelection, propertyExpression, out var configMeta))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
if (configMeta.LockedByConfigPluginId == Guid.Empty || !configMeta.IsLocked)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId);
|
var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.LockedByConfigPluginId);
|
||||||
if (plugin is null)
|
if (plugin is null)
|
||||||
{
|
{
|
||||||
configMeta.ResetManagedState();
|
configMeta.ResetLockedConfiguration();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -94,31 +94,31 @@ public sealed record Provider(
|
|||||||
provider = NONE;
|
provider = NONE;
|
||||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("InstanceName", out var instanceNameValue) || !instanceNameValue.TryRead<string>(out var instanceName))
|
if (!table.TryGetValue("InstanceName", out var instanceNameValue) || !instanceNameValue.TryRead<string>(out var instanceName))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid instance name.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid instance name. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
|
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid LLM provider enum value.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid LLM provider enum value. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
|
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid host enum value.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid host enum value. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
|
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid hostname.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,27 +127,27 @@ public sealed record Provider(
|
|||||||
{
|
{
|
||||||
if (!Enum.TryParse(hfInferenceProviderText, true, out hfInferenceProvider))
|
if (!Enum.TryParse(hfInferenceProviderText, true, out hfInferenceProvider))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid Hugging Face inference provider enum value.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid Hugging Face inference provider enum value. (Plugin ID: {configPluginId})");
|
||||||
hfInferenceProvider = HFInferenceProvider.NONE;
|
hfInferenceProvider = HFInferenceProvider.NONE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
|
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model table.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model table. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadModelTable(idx, modelTable, out var model))
|
if (!TryReadModelTable(idx, modelTable, configPluginId, out var model))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model configuration.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model configuration. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("AdditionalJsonApiParameters", out var additionalJsonApiParametersValue) || !additionalJsonApiParametersValue.TryRead<string>(out var additionalJsonApiParameters))
|
if (!table.TryGetValue("AdditionalJsonApiParameters", out var additionalJsonApiParametersValue) || !additionalJsonApiParametersValue.TryRead<string>(out var additionalJsonApiParameters))
|
||||||
{
|
{
|
||||||
// In this case, no reason exists to reject this provider, though.
|
// In this case, no reason exists to reject this provider, though.
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain valid additional JSON API parameters.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain valid additional JSON API parameters. (Plugin ID: {configPluginId})");
|
||||||
additionalJsonApiParameters = string.Empty;
|
additionalJsonApiParameters = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,7 +171,7 @@ public sealed record Provider(
|
|||||||
if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
|
if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
|
||||||
{
|
{
|
||||||
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
|
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
|
||||||
LOGGER.LogWarning($"The configured provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported.");
|
LOGGER.LogWarning($"The configured provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var encryption = PluginFactory.EnterpriseEncryption;
|
var encryption = PluginFactory.EnterpriseEncryption;
|
||||||
@ -185,31 +185,31 @@ public sealed record Provider(
|
|||||||
instanceName,
|
instanceName,
|
||||||
decryptedApiKey,
|
decryptedApiKey,
|
||||||
SecretStoreType.LLM_PROVIDER));
|
SecretStoreType.LLM_PROVIDER));
|
||||||
LOGGER.LogDebug($"Successfully decrypted API key for provider {idx}. It will be stored in the OS keyring.");
|
LOGGER.LogDebug($"Successfully decrypted API key for provider {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
LOGGER.LogWarning($"Failed to decrypt API key for provider {idx}. The encryption secret may be incorrect.");
|
LOGGER.LogWarning($"Failed to decrypt API key for provider {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
LOGGER.LogWarning($"The configured provider {idx} contains an encrypted API key, but no encryption secret is configured.");
|
LOGGER.LogWarning($"The configured provider {idx} contains an encrypted API key, but no encryption secret is configured. (Plugin ID: {configPluginId})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadModelTable(int idx, LuaTable table, out Model model)
|
private static bool TryReadModelTable(int idx, LuaTable table, Guid configPluginId, out Model model)
|
||||||
{
|
{
|
||||||
model = default;
|
model = default;
|
||||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
|
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model ID.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model ID. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
|
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model display name.");
|
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid model display name. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,43 +56,43 @@ public sealed record TranscriptionProvider(
|
|||||||
provider = NONE;
|
provider = NONE;
|
||||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid ID. The ID must be a valid GUID.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name))
|
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid name.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid name. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
|
if (!table.TryGetValue("UsedLLMProvider", out var usedLLMProviderValue) || !usedLLMProviderValue.TryRead<string>(out var usedLLMProviderText) || !Enum.TryParse<LLMProviders>(usedLLMProviderText, true, out var usedLLMProvider))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid LLM provider enum value.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid LLM provider enum value. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
|
if (!table.TryGetValue("Host", out var hostValue) || !hostValue.TryRead<string>(out var hostText) || !Enum.TryParse<Host>(hostText, true, out var host))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid host enum value.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid host enum value. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
|
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid hostname.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
|
if (!table.TryGetValue("Model", out var modelValue) || !modelValue.TryRead<LuaTable>(out var modelTable))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model table.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model table. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadModelTable(idx, modelTable, out var model))
|
if (!TryReadModelTable(idx, modelTable, configPluginId, out var model))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model configuration.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model configuration. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ public sealed record TranscriptionProvider(
|
|||||||
if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
|
if (table.TryGetValue("APIKey", out var apiKeyValue) && apiKeyValue.TryRead<string>(out var apiKeyText) && !string.IsNullOrWhiteSpace(apiKeyText))
|
||||||
{
|
{
|
||||||
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
|
if (!EnterpriseEncryption.IsEncrypted(apiKeyText))
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} contains a plaintext API key. Only encrypted API keys (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var encryption = PluginFactory.EnterpriseEncryption;
|
var encryption = PluginFactory.EnterpriseEncryption;
|
||||||
@ -128,31 +128,31 @@ public sealed record TranscriptionProvider(
|
|||||||
name,
|
name,
|
||||||
decryptedApiKey,
|
decryptedApiKey,
|
||||||
SecretStoreType.TRANSCRIPTION_PROVIDER));
|
SecretStoreType.TRANSCRIPTION_PROVIDER));
|
||||||
LOGGER.LogDebug($"Successfully decrypted API key for transcription provider {idx}. It will be stored in the OS keyring.");
|
LOGGER.LogDebug($"Successfully decrypted API key for transcription provider {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
LOGGER.LogWarning($"Failed to decrypt API key for transcription provider {idx}. The encryption secret may be incorrect.");
|
LOGGER.LogWarning($"Failed to decrypt API key for transcription provider {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} contains an encrypted API key, but no encryption secret is configured.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} contains an encrypted API key, but no encryption secret is configured. (Plugin ID: {configPluginId})");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadModelTable(int idx, LuaTable table, out Model model)
|
private static bool TryReadModelTable(int idx, LuaTable table, Guid configPluginId, out Model model)
|
||||||
{
|
{
|
||||||
model = default;
|
model = default;
|
||||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
|
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var id))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model ID.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model ID. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
|
if (!table.TryGetValue("DisplayName", out var displayNameValue) || !displayNameValue.TryRead<string>(out var displayName))
|
||||||
{
|
{
|
||||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model display name.");
|
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid model display name. (Plugin ID: {configPluginId})");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
|
using Markdig;
|
||||||
|
|
||||||
namespace AIStudio.Tools;
|
namespace AIStudio.Tools;
|
||||||
|
|
||||||
public static class Markdown
|
public static class Markdown
|
||||||
{
|
{
|
||||||
|
public static readonly MarkdownPipeline SAFE_MARKDOWN_PIPELINE = new MarkdownPipelineBuilder()
|
||||||
|
.UseAdvancedExtensions()
|
||||||
|
.DisableHtml()
|
||||||
|
.Build();
|
||||||
|
|
||||||
public static MudMarkdownProps DefaultConfig => new()
|
public static MudMarkdownProps DefaultConfig => new()
|
||||||
{
|
{
|
||||||
Heading =
|
Heading =
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
using AIStudio.Tools.Metadata;
|
using AIStudio.Tools.Metadata;
|
||||||
using AIStudio.Tools.Services;
|
using AIStudio.Tools.Services;
|
||||||
@ -74,36 +73,49 @@ public sealed class PandocProcessBuilder
|
|||||||
|
|
||||||
public async Task<PandocPreparedProcess> BuildAsync(RustService rustService)
|
public async Task<PandocPreparedProcess> BuildAsync(RustService rustService)
|
||||||
{
|
{
|
||||||
var sbArguments = new StringBuilder();
|
|
||||||
|
|
||||||
if (this.useStandaloneMode)
|
|
||||||
sbArguments.Append(" --standalone ");
|
|
||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(this.providedInputFile))
|
|
||||||
sbArguments.Append(this.providedInputFile);
|
|
||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(this.providedInputFormat))
|
|
||||||
sbArguments.Append($" -f {this.providedInputFormat}");
|
|
||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(this.providedOutputFormat))
|
|
||||||
sbArguments.Append($" -t {this.providedOutputFormat}");
|
|
||||||
|
|
||||||
foreach (var additionalArgument in this.additionalArguments)
|
|
||||||
sbArguments.Append($" {additionalArgument}");
|
|
||||||
|
|
||||||
if(!string.IsNullOrWhiteSpace(this.providedOutputFile))
|
|
||||||
sbArguments.Append($" -o {this.providedOutputFile}");
|
|
||||||
|
|
||||||
var pandocExecutable = await PandocExecutablePath(rustService);
|
var pandocExecutable = await PandocExecutablePath(rustService);
|
||||||
return new (new ProcessStartInfo
|
var startInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = pandocExecutable.Executable,
|
FileName = pandocExecutable.Executable,
|
||||||
Arguments = sbArguments.ToString(),
|
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
CreateNoWindow = true
|
CreateNoWindow = true
|
||||||
}, pandocExecutable.IsLocalInstallation);
|
};
|
||||||
|
|
||||||
|
// Use argument tokens instead of a single command string so paths with spaces
|
||||||
|
// or Unicode characters are passed to Pandoc unchanged on all platforms.
|
||||||
|
if (this.useStandaloneMode)
|
||||||
|
startInfo.ArgumentList.Add("--standalone");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(this.providedInputFile))
|
||||||
|
startInfo.ArgumentList.Add(this.providedInputFile);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(this.providedInputFormat))
|
||||||
|
{
|
||||||
|
startInfo.ArgumentList.Add("-f");
|
||||||
|
startInfo.ArgumentList.Add(this.providedInputFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(this.providedOutputFormat))
|
||||||
|
{
|
||||||
|
startInfo.ArgumentList.Add("-t");
|
||||||
|
startInfo.ArgumentList.Add(this.providedOutputFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var additionalArgument in this.additionalArguments)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(additionalArgument))
|
||||||
|
startInfo.ArgumentList.Add(additionalArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(this.providedOutputFile))
|
||||||
|
{
|
||||||
|
startInfo.ArgumentList.Add("-o");
|
||||||
|
startInfo.ArgumentList.Add(this.providedOutputFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(startInfo, pandocExecutable.IsLocalInstallation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -3,4 +3,8 @@ namespace AIStudio.Tools.PluginSystem;
|
|||||||
public interface IAvailablePlugin : IPluginMetadata
|
public interface IAvailablePlugin : IPluginMetadata
|
||||||
{
|
{
|
||||||
public string LocalPath { get; }
|
public string LocalPath { get; }
|
||||||
|
|
||||||
|
public bool IsManagedByConfigServer { get; }
|
||||||
|
|
||||||
|
public Guid? ManagedConfigurationId { get; }
|
||||||
}
|
}
|
||||||
@ -18,6 +18,11 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<PluginConfigurationObject> ConfigObjects => this.configObjects;
|
public IEnumerable<PluginConfigurationObject> ConfigObjects => this.configObjects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True/false when explicitly configured in the plugin, otherwise null.
|
||||||
|
/// </summary>
|
||||||
|
public bool? DeployedUsingConfigServer { get; } = ReadDeployedUsingConfigServer(state);
|
||||||
|
|
||||||
public async Task InitializeAsync(bool dryRun)
|
public async Task InitializeAsync(bool dryRun)
|
||||||
{
|
{
|
||||||
if(!this.TryProcessConfiguration(dryRun, out var issue))
|
if(!this.TryProcessConfiguration(dryRun, out var issue))
|
||||||
@ -69,6 +74,14 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private sealed record TemporarySecretId(string SecretId, string SecretName) : ISecretId;
|
private sealed record TemporarySecretId(string SecretId, string SecretName) : ISecretId;
|
||||||
|
|
||||||
|
private static bool? ReadDeployedUsingConfigServer(LuaState state)
|
||||||
|
{
|
||||||
|
if (state.Environment["DEPLOYED_USING_CONFIG_SERVER"].TryRead<bool>(out var deployedUsingConfigServer))
|
||||||
|
return deployedUsingConfigServer;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to initialize the UI text content of the plugin.
|
/// Tries to initialize the UI text content of the plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -108,8 +121,8 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
// Config: preview features visibility
|
// Config: preview features visibility
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreviewVisibility, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
// Config: enabled preview features
|
// Config: enabled preview features (plugin contribution; users can enable additional features)
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.EnabledPreviewFeatures, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfigurationWithPluginContribution(x => x.App, x => x.EnabledPreviewFeatures, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
// Config: hide some assistants?
|
// Config: hide some assistants?
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HiddenAssistants, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.HiddenAssistants, this.Id, settingsTable, dryRun);
|
||||||
@ -135,6 +148,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
// Handle configured document analysis policies:
|
// Handle configured document analysis policies:
|
||||||
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, x => x.NextDocumentAnalysisPolicyNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
PluginConfigurationObject.TryParse(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, x => x.NextDocumentAnalysisPolicyNum, mainTable, this.Id, ref this.configObjects, dryRun);
|
||||||
|
|
||||||
|
// Config: preselected provider?
|
||||||
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
// Config: preselected profile?
|
// Config: preselected profile?
|
||||||
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProfile, Guid.Empty, this.Id, settingsTable, dryRun);
|
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProfile, Guid.Empty, this.Id, settingsTable, dryRun);
|
||||||
|
|
||||||
|
|||||||
@ -79,13 +79,13 @@ public sealed record PluginConfigurationObject
|
|||||||
|
|
||||||
if (luaTableName is null)
|
if (luaTableName is null)
|
||||||
{
|
{
|
||||||
LOG.LogError($"The configuration object type '{configObjectType}' is not supported yet.");
|
LOG.LogError("The configuration object type '{ConfigObjectType}' is not supported yet (config plugin id: {ConfigPluginId}).", configObjectType, configPluginId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mainTable.TryGetValue(luaTableName, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable))
|
if (!mainTable.TryGetValue(luaTableName, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable))
|
||||||
{
|
{
|
||||||
LOG.LogWarning($"The {luaTableName} table does not exist or is not a valid table.");
|
LOG.LogWarning("The table '{LuaTableName}' does not exist or is not a valid table (config plugin id: {ConfigPluginId}).", luaTableName, configPluginId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +97,7 @@ public sealed record PluginConfigurationObject
|
|||||||
var luaObjectTableValue = luaTable[i];
|
var luaObjectTableValue = luaTable[i];
|
||||||
if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable))
|
if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable))
|
||||||
{
|
{
|
||||||
LOG.LogWarning($"The {luaObjectTable} table at index {i} is not a valid table.");
|
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", luaTableName, i, configPluginId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,12 +151,12 @@ public sealed record PluginConfigurationObject
|
|||||||
random ??= new ThreadSafeRandom();
|
random ??= new ThreadSafeRandom();
|
||||||
configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) };
|
configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) };
|
||||||
storedObjects.Add((TClass)configObject);
|
storedObjects.Add((TClass)configObject);
|
||||||
LOG.LogWarning($"The next number for the configuration object '{configObject.Name}' (id={configObject.Id}) could not be incremented. Using a random number instead.");
|
LOG.LogWarning("The next number for the configuration object '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
LOG.LogWarning($"The {luaObjectTable} table at index {i} does not contain a valid chat template configuration.");
|
LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} does not contain a valid configuration object (type={ConfigObjectType}, config plugin id: {ConfigPluginId}).", luaTableName, i, configObjectType, configPluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -5,10 +5,10 @@ namespace AIStudio.Tools.PluginSystem;
|
|||||||
|
|
||||||
public static partial class PluginFactory
|
public static partial class PluginFactory
|
||||||
{
|
{
|
||||||
public static async Task<EntityTagHeaderValue?> DetermineConfigPluginETagAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default)
|
public static async Task<(bool Success, EntityTagHeaderValue? ETag, string? Issue)> DetermineConfigPluginETagAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if(configPlugId == Guid.Empty || string.IsNullOrWhiteSpace(configServerUrl))
|
if(configPlugId == Guid.Empty || string.IsNullOrWhiteSpace(configServerUrl))
|
||||||
return null;
|
return (false, null, "Configuration ID or server URL is missing.");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -18,18 +18,24 @@ public static partial class PluginFactory
|
|||||||
using var http = new HttpClient();
|
using var http = new HttpClient();
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl);
|
using var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl);
|
||||||
var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
return response.Headers.ETag;
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
LOG.LogError($"Failed to determine the ETag for configuration plugin '{configPlugId}'. HTTP Status: {response.StatusCode}");
|
||||||
|
return (false, null, $"HTTP status: {response.StatusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, response.Headers.ETag, null);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
LOG.LogError(e, "An error occurred while determining the ETag for the configuration plugin.");
|
LOG.LogError(e, "An error occurred while determining the ETag for the configuration plugin.");
|
||||||
return null;
|
return (false, null, e.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<bool> TryDownloadingConfigPluginAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default)
|
public static async Task<bool> TryDownloadingConfigPluginAsync(Guid configPlugId, string configServerUrl, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if(!IS_INITIALIZED)
|
if(!IsInitialized)
|
||||||
{
|
{
|
||||||
LOG.LogWarning("Plugin factory is not yet initialized. Cannot download configuration plugin.");
|
LOG.LogWarning("Plugin factory is not yet initialized. Cannot download configuration plugin.");
|
||||||
return false;
|
return false;
|
||||||
@ -40,36 +46,72 @@ public static partial class PluginFactory
|
|||||||
|
|
||||||
LOG.LogInformation($"Try to download configuration plugin with ID='{configPlugId}' from server='{configServerUrl}' (GET {downloadUrl})");
|
LOG.LogInformation($"Try to download configuration plugin with ID='{configPlugId}' from server='{configServerUrl}' (GET {downloadUrl})");
|
||||||
var tempDownloadFile = Path.GetTempFileName();
|
var tempDownloadFile = Path.GetTempFileName();
|
||||||
|
var stagedDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, $"{configPlugId}.staging-{Guid.NewGuid():N}");
|
||||||
|
string? backupDirectory = null;
|
||||||
|
var wasSuccessful = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await LockHotReloadAsync();
|
await LockHotReloadAsync();
|
||||||
using var httpClient = new HttpClient();
|
using var httpClient = new HttpClient();
|
||||||
var response = await httpClient.GetAsync(downloadUrl, cancellationToken);
|
var response = await httpClient.GetAsync(downloadUrl, cancellationToken);
|
||||||
if (response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
LOG.LogError($"Failed to download the enterprise configuration plugin. HTTP Status: {response.StatusCode}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
await using(var tempFileStream = File.Create(tempDownloadFile))
|
await using(var tempFileStream = File.Create(tempDownloadFile))
|
||||||
{
|
{
|
||||||
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
|
await response.Content.CopyToAsync(tempFileStream, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ZipFile.ExtractToDirectory(tempDownloadFile, stagedDirectory);
|
||||||
|
|
||||||
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
|
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
|
||||||
if (Directory.Exists(configDirectory))
|
if (Directory.Exists(configDirectory))
|
||||||
Directory.Delete(configDirectory, true);
|
{
|
||||||
|
backupDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, $"{configPlugId}.backup-{Guid.NewGuid():N}");
|
||||||
|
Directory.Move(configDirectory, backupDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(configDirectory);
|
Directory.Move(stagedDirectory, configDirectory);
|
||||||
ZipFile.ExtractToDirectory(tempDownloadFile, configDirectory);
|
if (!string.IsNullOrWhiteSpace(backupDirectory) && Directory.Exists(backupDirectory))
|
||||||
|
Directory.Delete(backupDirectory, true);
|
||||||
|
|
||||||
LOG.LogInformation($"Configuration plugin with ID='{configPlugId}' downloaded and extracted successfully to '{configDirectory}'.");
|
LOG.LogInformation($"Configuration plugin with ID='{configPlugId}' downloaded and extracted successfully to '{configDirectory}'.");
|
||||||
}
|
wasSuccessful = true;
|
||||||
else
|
|
||||||
LOG.LogError($"Failed to download the enterprise configuration plugin. HTTP Status: {response.StatusCode}");
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
LOG.LogError(e, "An error occurred while downloading or extracting the enterprise configuration plugin.");
|
LOG.LogError(e, "An error occurred while downloading or extracting the enterprise configuration plugin.");
|
||||||
|
|
||||||
|
var configDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, configPlugId.ToString());
|
||||||
|
if (!string.IsNullOrWhiteSpace(backupDirectory) && Directory.Exists(backupDirectory) && !Directory.Exists(configDirectory))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Move(backupDirectory, configDirectory);
|
||||||
|
}
|
||||||
|
catch (Exception restoreException)
|
||||||
|
{
|
||||||
|
LOG.LogError(restoreException, "Failed to restore the previous configuration plugin after a failed update.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
if (Directory.Exists(stagedDirectory))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(stagedDirectory, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LOG.LogError(e, "Failed to delete the staged configuration plugin directory.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (File.Exists(tempDownloadFile))
|
if (File.Exists(tempDownloadFile))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -85,6 +127,6 @@ public static partial class PluginFactory
|
|||||||
UnlockHotReload();
|
UnlockHotReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return wasSuccessful;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,7 +6,7 @@ public static partial class PluginFactory
|
|||||||
|
|
||||||
public static void SetUpHotReloading()
|
public static void SetUpHotReloading()
|
||||||
{
|
{
|
||||||
if (!IS_INITIALIZED)
|
if (!IsInitialized)
|
||||||
{
|
{
|
||||||
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ public static partial class PluginFactory
|
|||||||
{
|
{
|
||||||
public static async Task EnsureInternalPlugins()
|
public static async Task EnsureInternalPlugins()
|
||||||
{
|
{
|
||||||
if (!IS_INITIALIZED)
|
if (!IsInitialized)
|
||||||
{
|
{
|
||||||
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -30,7 +30,7 @@ public static partial class PluginFactory
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static async Task LoadAll(CancellationToken cancellationToken = default)
|
public static async Task LoadAll(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!IS_INITIALIZED)
|
if (!IsInitialized)
|
||||||
{
|
{
|
||||||
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
||||||
return;
|
return;
|
||||||
@ -104,16 +104,40 @@ public static partial class PluginFactory
|
|||||||
|
|
||||||
LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')");
|
LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')");
|
||||||
|
|
||||||
// For configuration plugins, validate that the plugin ID matches the enterprise config ID
|
var isConfigurationPluginInConfigDirectory =
|
||||||
// (the directory name under which the plugin was downloaded):
|
plugin.Type is PluginType.CONFIGURATION &&
|
||||||
if (plugin.Type is PluginType.CONFIGURATION && pluginPath.StartsWith(CONFIGURATION_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase))
|
pluginPath.StartsWith(CONFIGURATION_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var isManagedByConfigServer = false;
|
||||||
|
Guid? managedConfigurationId = null;
|
||||||
|
if (plugin is PluginConfiguration configPlugin)
|
||||||
{
|
{
|
||||||
var directoryName = Path.GetFileName(pluginPath);
|
if (configPlugin.DeployedUsingConfigServer.HasValue)
|
||||||
if (Guid.TryParse(directoryName, out var enterpriseConfigId) && enterpriseConfigId != plugin.Id)
|
isManagedByConfigServer = configPlugin.DeployedUsingConfigServer.Value;
|
||||||
LOG.LogWarning($"The configuration plugin's ID ('{plugin.Id}') does not match the enterprise configuration ID ('{enterpriseConfigId}'). These IDs should be identical. Please update the plugin's ID field to match the enterprise configuration ID.");
|
|
||||||
|
else if (isConfigurationPluginInConfigDirectory)
|
||||||
|
{
|
||||||
|
isManagedByConfigServer = true;
|
||||||
|
LOG.LogWarning($"The configuration plugin '{plugin.Id}' does not define 'DEPLOYED_USING_CONFIG_SERVER'. Falling back to the plugin path and treating it as managed because it is stored under '{CONFIGURATION_PLUGINS_ROOT}'.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath));
|
// For configuration plugins, validate that the plugin ID matches the enterprise config ID
|
||||||
|
// (the directory name under which the plugin was downloaded):
|
||||||
|
if (isConfigurationPluginInConfigDirectory && isManagedByConfigServer)
|
||||||
|
{
|
||||||
|
var directoryName = Path.GetFileName(pluginPath);
|
||||||
|
if (Guid.TryParse(directoryName, out var enterpriseConfigId))
|
||||||
|
{
|
||||||
|
managedConfigurationId = enterpriseConfigId;
|
||||||
|
if (enterpriseConfigId != plugin.Id)
|
||||||
|
LOG.LogWarning($"The configuration plugin's ID ('{plugin.Id}') does not match the enterprise configuration ID ('{enterpriseConfigId}'). These IDs should be identical. Please update the plugin's ID field to match the enterprise configuration ID.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
LOG.LogWarning($"Could not determine the managed configuration ID for configuration plugin '{plugin.Id}'. The plugin directory '{pluginPath}' does not end with a valid GUID.");
|
||||||
|
}
|
||||||
|
|
||||||
|
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath, isManagedByConfigServer, managedConfigurationId));
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -163,6 +187,10 @@ public static partial class PluginFactory
|
|||||||
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList))
|
if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
|
// Check for a preselected provider:
|
||||||
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProvider, AVAILABLE_PLUGINS))
|
||||||
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check for a preselected profile:
|
// Check for a preselected profile:
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProfile, AVAILABLE_PLUGINS))
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProfile, AVAILABLE_PLUGINS))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
@ -191,6 +219,9 @@ public static partial class PluginFactory
|
|||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS))
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
|
if(ManagedConfiguration.IsPluginContributionLeftOver(x => x.App, x => x.EnabledPreviewFeatures, AVAILABLE_PLUGINS))
|
||||||
|
wasConfigurationChanged = true;
|
||||||
|
|
||||||
// Check for the transcription provider:
|
// Check for the transcription provider:
|
||||||
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UseTranscriptionProvider, AVAILABLE_PLUGINS))
|
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UseTranscriptionProvider, AVAILABLE_PLUGINS))
|
||||||
wasConfigurationChanged = true;
|
wasConfigurationChanged = true;
|
||||||
|
|||||||
@ -1,41 +1,118 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace AIStudio.Tools.PluginSystem;
|
namespace AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
public static partial class PluginFactory
|
public static partial class PluginFactory
|
||||||
{
|
{
|
||||||
public static void RemovePluginAsync(Guid pluginId)
|
private const string REASON_NO_LONGER_REFERENCED = "no longer referenced by active enterprise environments";
|
||||||
|
|
||||||
|
public static void RemoveUnreferencedManagedConfigurationPlugins(ISet<Guid> activeConfigurationIds)
|
||||||
{
|
{
|
||||||
if (!IS_INITIALIZED)
|
if (!IsInitialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LOG.LogWarning($"Try to remove plugin with ID: {pluginId}");
|
var pluginIdsToRemove = new HashSet<Guid>();
|
||||||
|
|
||||||
|
// Case 1: Plugins are already loaded and metadata is available.
|
||||||
|
foreach (var plugin in AVAILABLE_PLUGINS.Where(plugin =>
|
||||||
|
plugin.Type is PluginType.CONFIGURATION &&
|
||||||
|
plugin.IsManagedByConfigServer &&
|
||||||
|
!activeConfigurationIds.Contains(plugin.Id)))
|
||||||
|
pluginIdsToRemove.Add(plugin.Id);
|
||||||
|
|
||||||
|
// Case 2: Startup cleanup before the initial plugin load.
|
||||||
|
// In this case, we inspect the .config directories directly.
|
||||||
|
if (Directory.Exists(CONFIGURATION_PLUGINS_ROOT))
|
||||||
|
{
|
||||||
|
foreach (var pluginDirectory in Directory.EnumerateDirectories(CONFIGURATION_PLUGINS_ROOT))
|
||||||
|
{
|
||||||
|
var directoryName = Path.GetFileName(pluginDirectory);
|
||||||
|
if (!Guid.TryParse(directoryName, out var pluginId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (activeConfigurationIds.Contains(pluginId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var deployFlag = ReadDeployFlagFromPluginFile(pluginDirectory);
|
||||||
|
var isManagedByConfigServer = deployFlag ?? true;
|
||||||
|
if (!deployFlag.HasValue)
|
||||||
|
LOG.LogWarning($"Configuration plugin '{pluginId}' does not define 'DEPLOYED_USING_CONFIG_SERVER'. Falling back to the plugin path and treating it as managed because it is stored under '{CONFIGURATION_PLUGINS_ROOT}'.");
|
||||||
|
|
||||||
|
if (isManagedByConfigServer)
|
||||||
|
pluginIdsToRemove.Add(pluginId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pluginId in pluginIdsToRemove)
|
||||||
|
RemovePluginAsync(pluginId, REASON_NO_LONGER_REFERENCED);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemovePluginAsync(Guid pluginId, string reason)
|
||||||
|
{
|
||||||
|
if (!IsInitialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG.LogWarning("Removing plugin with ID '{PluginId}'. Reason: {Reason}.", pluginId, reason);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Remove the plugin from the available plugins list:
|
// Remove the plugin from the available plugins list:
|
||||||
//
|
//
|
||||||
var availablePluginToRemove = AVAILABLE_PLUGINS.FirstOrDefault(p => p.Id == pluginId);
|
var availablePluginToRemove = AVAILABLE_PLUGINS.FirstOrDefault(p => p.Id == pluginId);
|
||||||
if (availablePluginToRemove == null)
|
if (availablePluginToRemove != null)
|
||||||
{
|
|
||||||
LOG.LogWarning($"No plugin found with ID: {pluginId}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AVAILABLE_PLUGINS.Remove(availablePluginToRemove);
|
AVAILABLE_PLUGINS.Remove(availablePluginToRemove);
|
||||||
|
else
|
||||||
|
LOG.LogWarning("No available plugin found with ID '{PluginId}' while removing plugin. Reason: {Reason}.", pluginId, reason);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Remove the plugin from the running plugins list:
|
// Remove the plugin from the running plugins list:
|
||||||
//
|
//
|
||||||
var runningPluginToRemove = RUNNING_PLUGINS.FirstOrDefault(p => p.Id == pluginId);
|
var runningPluginToRemove = RUNNING_PLUGINS.FirstOrDefault(p => p.Id == pluginId);
|
||||||
if (runningPluginToRemove == null)
|
if (runningPluginToRemove == null)
|
||||||
LOG.LogWarning($"No running plugin found with ID: {pluginId}");
|
LOG.LogWarning("No running plugin found with ID '{PluginId}' while removing plugin. Reason: {Reason}.", pluginId, reason);
|
||||||
else
|
else
|
||||||
RUNNING_PLUGINS.Remove(runningPluginToRemove);
|
RUNNING_PLUGINS.Remove(runningPluginToRemove);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Delete the plugin directory:
|
// Delete the plugin directory:
|
||||||
//
|
//
|
||||||
var pluginDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, availablePluginToRemove.Id.ToString());
|
DeleteConfigurationPluginDirectory(pluginId);
|
||||||
if (Directory.Exists(pluginDirectory))
|
|
||||||
|
LOG.LogInformation("Plugin with ID '{PluginId}' removed successfully. Reason: {Reason}.", pluginId, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool? ReadDeployFlagFromPluginFile(string pluginDirectory)
|
||||||
{
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pluginFile = Path.Join(pluginDirectory, "plugin.lua");
|
||||||
|
if (!File.Exists(pluginFile))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var pluginCode = File.ReadAllText(pluginFile);
|
||||||
|
var match = DeployedByConfigServerRegex().Match(pluginCode);
|
||||||
|
if (!match.Success)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return bool.TryParse(match.Groups[1].Value, out var deployFlag)
|
||||||
|
? deployFlag
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LOG.LogWarning(ex, $"Failed to parse deployment flag from plugin directory '{pluginDirectory}'.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteConfigurationPluginDirectory(Guid pluginId)
|
||||||
|
{
|
||||||
|
var pluginDirectory = Path.Join(CONFIGURATION_PLUGINS_ROOT, pluginId.ToString());
|
||||||
|
if (!Directory.Exists(pluginDirectory))
|
||||||
|
{
|
||||||
|
LOG.LogWarning($"Plugin directory '{pluginDirectory}' does not exist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.Delete(pluginDirectory, true);
|
Directory.Delete(pluginDirectory, true);
|
||||||
@ -46,9 +123,7 @@ public static partial class PluginFactory
|
|||||||
LOG.LogError(ex, $"Failed to delete plugin directory '{pluginDirectory}'.");
|
LOG.LogError(ex, $"Failed to delete plugin directory '{pluginDirectory}'.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
LOG.LogWarning($"Plugin directory '{pluginDirectory}' does not exist.");
|
|
||||||
|
|
||||||
LOG.LogInformation($"Plugin with ID: {pluginId} removed successfully.");
|
[GeneratedRegex(@"^\s*DEPLOYED_USING_CONFIG_SERVER\s*=\s*(true|false)\s*(?:--.*)?$", RegexOptions.IgnoreCase | RegexOptions.Multiline)]
|
||||||
}
|
private static partial Regex DeployedByConfigServerRegex();
|
||||||
}
|
}
|
||||||
@ -34,7 +34,7 @@ public static partial class PluginFactory
|
|||||||
|
|
||||||
if (startedBasePlugin is PluginLanguage languagePlugin)
|
if (startedBasePlugin is PluginLanguage languagePlugin)
|
||||||
{
|
{
|
||||||
BASE_LANGUAGE_PLUGIN = languagePlugin;
|
BaseLanguage = languagePlugin;
|
||||||
RUNNING_PLUGINS.Add(languagePlugin);
|
RUNNING_PLUGINS.Add(languagePlugin);
|
||||||
LOG.LogInformation($"Successfully started the base language plugin: Id='{languagePlugin.Id}', Type='{languagePlugin.Type}', Name='{languagePlugin.Name}', Version='{languagePlugin.Version}'");
|
LOG.LogInformation($"Successfully started the base language plugin: Id='{languagePlugin.Id}', Type='{languagePlugin.Type}', Name='{languagePlugin.Name}', Version='{languagePlugin.Version}'");
|
||||||
}
|
}
|
||||||
@ -44,7 +44,7 @@ public static partial class PluginFactory
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
LOG.LogError(e, $"An error occurred while starting the base language plugin: Id='{baseLanguagePluginId}'.");
|
LOG.LogError(e, $"An error occurred while starting the base language plugin: Id='{baseLanguagePluginId}'.");
|
||||||
BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE;
|
BaseLanguage = NoPluginLanguage.INSTANCE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,8 +106,8 @@ public static partial class PluginFactory
|
|||||||
//
|
//
|
||||||
// When this is a language plugin, we need to set the base language plugin.
|
// When this is a language plugin, we need to set the base language plugin.
|
||||||
//
|
//
|
||||||
if (plugin is PluginLanguage languagePlugin && BASE_LANGUAGE_PLUGIN != NoPluginLanguage.INSTANCE)
|
if (plugin is PluginLanguage languagePlugin && BaseLanguage != NoPluginLanguage.INSTANCE)
|
||||||
languagePlugin.SetBaseLanguage(BASE_LANGUAGE_PLUGIN);
|
languagePlugin.SetBaseLanguage(BaseLanguage);
|
||||||
|
|
||||||
if(plugin is PluginConfiguration configPlugin)
|
if(plugin is PluginConfiguration configPlugin)
|
||||||
await configPlugin.InitializeAsync(false);
|
await configPlugin.InitializeAsync(false);
|
||||||
|
|||||||
@ -7,16 +7,16 @@ public static partial class PluginFactory
|
|||||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory));
|
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory));
|
||||||
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||||
|
|
||||||
private static bool IS_INITIALIZED;
|
|
||||||
private static string DATA_DIR = string.Empty;
|
private static string DATA_DIR = string.Empty;
|
||||||
private static string PLUGINS_ROOT = string.Empty;
|
private static string PLUGINS_ROOT = string.Empty;
|
||||||
private static string INTERNAL_PLUGINS_ROOT = string.Empty;
|
private static string INTERNAL_PLUGINS_ROOT = string.Empty;
|
||||||
private static string CONFIGURATION_PLUGINS_ROOT = string.Empty;
|
private static string CONFIGURATION_PLUGINS_ROOT = string.Empty;
|
||||||
private static string HOT_RELOAD_LOCK_FILE = string.Empty;
|
private static string HOT_RELOAD_LOCK_FILE = string.Empty;
|
||||||
private static FileSystemWatcher HOT_RELOAD_WATCHER = null!;
|
private static FileSystemWatcher HOT_RELOAD_WATCHER = null!;
|
||||||
private static ILanguagePlugin BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE;
|
|
||||||
|
|
||||||
public static ILanguagePlugin BaseLanguage => BASE_LANGUAGE_PLUGIN;
|
public static ILanguagePlugin BaseLanguage { get; private set; } = NoPluginLanguage.INSTANCE;
|
||||||
|
|
||||||
|
public static bool IsInitialized { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the enterprise encryption instance for decrypting API keys in configuration plugins.
|
/// Gets the enterprise encryption instance for decrypting API keys in configuration plugins.
|
||||||
@ -47,7 +47,7 @@ public static partial class PluginFactory
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool Setup()
|
public static bool Setup()
|
||||||
{
|
{
|
||||||
if(IS_INITIALIZED)
|
if(IsInitialized)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
LOG.LogInformation("Initializing plugin factory...");
|
LOG.LogInformation("Initializing plugin factory...");
|
||||||
@ -61,14 +61,14 @@ public static partial class PluginFactory
|
|||||||
Directory.CreateDirectory(PLUGINS_ROOT);
|
Directory.CreateDirectory(PLUGINS_ROOT);
|
||||||
|
|
||||||
HOT_RELOAD_WATCHER = new(PLUGINS_ROOT);
|
HOT_RELOAD_WATCHER = new(PLUGINS_ROOT);
|
||||||
IS_INITIALIZED = true;
|
IsInitialized = true;
|
||||||
LOG.LogInformation("Plugin factory initialized successfully.");
|
LOG.LogInformation("Plugin factory initialized successfully.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task LockHotReloadAsync()
|
private static async Task LockHotReloadAsync()
|
||||||
{
|
{
|
||||||
if (!IS_INITIALIZED)
|
if (!IsInitialized)
|
||||||
{
|
{
|
||||||
LOG.LogError("PluginFactory is not initialized.");
|
LOG.LogError("PluginFactory is not initialized.");
|
||||||
return;
|
return;
|
||||||
@ -92,7 +92,7 @@ public static partial class PluginFactory
|
|||||||
|
|
||||||
private static void UnlockHotReload()
|
private static void UnlockHotReload()
|
||||||
{
|
{
|
||||||
if (!IS_INITIALIZED)
|
if (!IsInitialized)
|
||||||
{
|
{
|
||||||
LOG.LogError("PluginFactory is not initialized.");
|
LOG.LogError("PluginFactory is not initialized.");
|
||||||
return;
|
return;
|
||||||
@ -113,7 +113,7 @@ public static partial class PluginFactory
|
|||||||
|
|
||||||
public static void Dispose()
|
public static void Dispose()
|
||||||
{
|
{
|
||||||
if(!IS_INITIALIZED)
|
if(!IsInitialized)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
HOT_RELOAD_WATCHER.Dispose();
|
HOT_RELOAD_WATCHER.Dispose();
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
namespace AIStudio.Tools.PluginSystem;
|
namespace AIStudio.Tools.PluginSystem;
|
||||||
|
|
||||||
public sealed class PluginMetadata(PluginBase plugin, string localPath) : IAvailablePlugin
|
public sealed class PluginMetadata(PluginBase plugin, string localPath, bool isManagedByConfigServer = false, Guid? managedConfigurationId = null) : IAvailablePlugin
|
||||||
{
|
{
|
||||||
#region Implementation of IPluginMetadata
|
#region Implementation of IPluginMetadata
|
||||||
|
|
||||||
@ -52,5 +52,9 @@ public sealed class PluginMetadata(PluginBase plugin, string localPath) : IAvail
|
|||||||
|
|
||||||
public string LocalPath { get; } = localPath;
|
public string LocalPath { get; } = localPath;
|
||||||
|
|
||||||
|
public bool IsManagedByConfigServer { get; } = isManagedByConfigServer;
|
||||||
|
|
||||||
|
public Guid? ManagedConfigurationId { get; } = managedConfigurationId;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@ -6,6 +6,8 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
{
|
{
|
||||||
public static List<EnterpriseEnvironment> CURRENT_ENVIRONMENTS = [];
|
public static List<EnterpriseEnvironment> CURRENT_ENVIRONMENTS = [];
|
||||||
|
|
||||||
|
public static bool HasValidEnterpriseSnapshot { get; private set; }
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromMinutes(6);
|
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromMinutes(6);
|
||||||
#else
|
#else
|
||||||
@ -33,34 +35,10 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger.LogInformation("Start updating of the enterprise environment.");
|
logger.LogInformation("Start updating of the enterprise environment.");
|
||||||
|
HasValidEnterpriseSnapshot = false;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Step 1: Handle deletions first.
|
// Step 1: Fetch all active configurations.
|
||||||
//
|
|
||||||
List<Guid> deleteConfigIds;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
deleteConfigIds = await rustService.EnterpriseEnvDeleteConfigIds();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
logger.LogError(e, "Failed to fetch the enterprise delete configuration IDs from the Rust service.");
|
|
||||||
await MessageBus.INSTANCE.SendMessage(null, Event.RUST_SERVICE_UNAVAILABLE, "EnterpriseEnvDeleteConfigIds failed");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var deleteId in deleteConfigIds)
|
|
||||||
{
|
|
||||||
var isPluginInUse = PluginFactory.AvailablePlugins.Any(plugin => plugin.Id == deleteId);
|
|
||||||
if (isPluginInUse)
|
|
||||||
{
|
|
||||||
logger.LogWarning("The enterprise environment configuration ID '{DeleteConfigId}' must be removed.", deleteId);
|
|
||||||
PluginFactory.RemovePluginAsync(deleteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Step 2: Fetch all active configurations.
|
|
||||||
//
|
//
|
||||||
List<EnterpriseEnvironment> fetchedConfigs;
|
List<EnterpriseEnvironment> fetchedConfigs;
|
||||||
try
|
try
|
||||||
@ -75,9 +53,20 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Step 3: Determine ETags and build the next environment list.
|
// Step 2: Determine ETags and build the list of reachable configurations.
|
||||||
|
// IMPORTANT: when one config server fails, we continue with the others.
|
||||||
//
|
//
|
||||||
var nextEnvironments = new List<EnterpriseEnvironment>();
|
var reachableEnvironments = new List<EnterpriseEnvironment>();
|
||||||
|
var failedConfigIds = new HashSet<Guid>();
|
||||||
|
var currentEnvironmentsById = CURRENT_ENVIRONMENTS
|
||||||
|
.GroupBy(env => env.ConfigurationId)
|
||||||
|
.ToDictionary(group => group.Key, group => group.Last());
|
||||||
|
|
||||||
|
var activeFetchedEnvironmentsById = fetchedConfigs
|
||||||
|
.Where(config => config.IsActive)
|
||||||
|
.GroupBy(config => config.ConfigurationId)
|
||||||
|
.ToDictionary(group => group.Key, group => group.Last());
|
||||||
|
|
||||||
foreach (var config in fetchedConfigs)
|
foreach (var config in fetchedConfigs)
|
||||||
{
|
{
|
||||||
if (!config.IsActive)
|
if (!config.IsActive)
|
||||||
@ -86,72 +75,98 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var etag = await PluginFactory.DetermineConfigPluginETagAsync(config.ConfigurationId, config.ConfigurationServerUrl);
|
var etagResponse = await PluginFactory.DetermineConfigPluginETagAsync(config.ConfigurationId, config.ConfigurationServerUrl);
|
||||||
nextEnvironments.Add(config with { ETag = etag });
|
if (!etagResponse.Success)
|
||||||
}
|
|
||||||
|
|
||||||
if (nextEnvironments.Count == 0)
|
|
||||||
{
|
{
|
||||||
if (CURRENT_ENVIRONMENTS.Count > 0)
|
failedConfigIds.Add(config.ConfigurationId);
|
||||||
{
|
logger.LogWarning("Failed to read enterprise config metadata for '{ConfigId}' from '{ServerUrl}': {Issue}. Keeping the current plugin state for this configuration.", config.ConfigurationId, config.ConfigurationServerUrl, etagResponse.Issue ?? "Unknown issue");
|
||||||
logger.LogWarning("AI Studio no longer has any enterprise configurations. Removing previously active configs.");
|
|
||||||
|
|
||||||
// Remove plugins for configs that were previously active:
|
|
||||||
foreach (var oldEnv in CURRENT_ENVIRONMENTS)
|
|
||||||
{
|
|
||||||
var isPluginInUse = PluginFactory.AvailablePlugins.Any(plugin => plugin.Id == oldEnv.ConfigurationId);
|
|
||||||
if (isPluginInUse)
|
|
||||||
PluginFactory.RemovePluginAsync(oldEnv.ConfigurationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
logger.LogInformation("AI Studio runs without any enterprise configurations.");
|
|
||||||
|
|
||||||
CURRENT_ENVIRONMENTS = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Step 4: Compare with current environments and process changes.
|
|
||||||
//
|
|
||||||
var currentIds = CURRENT_ENVIRONMENTS.Select(e => e.ConfigurationId).ToHashSet();
|
|
||||||
var nextIds = nextEnvironments.Select(e => e.ConfigurationId).ToHashSet();
|
|
||||||
|
|
||||||
// Remove plugins for configs that are no longer present:
|
|
||||||
foreach (var oldEnv in CURRENT_ENVIRONMENTS)
|
|
||||||
{
|
|
||||||
if (!nextIds.Contains(oldEnv.ConfigurationId))
|
|
||||||
{
|
|
||||||
logger.LogInformation("Enterprise configuration '{ConfigId}' was removed.", oldEnv.ConfigurationId);
|
|
||||||
var isPluginInUse = PluginFactory.AvailablePlugins.Any(plugin => plugin.Id == oldEnv.ConfigurationId);
|
|
||||||
if (isPluginInUse)
|
|
||||||
PluginFactory.RemovePluginAsync(oldEnv.ConfigurationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process new or changed configs:
|
|
||||||
foreach (var nextEnv in nextEnvironments)
|
|
||||||
{
|
|
||||||
var currentEnv = CURRENT_ENVIRONMENTS.FirstOrDefault(e => e.ConfigurationId == nextEnv.ConfigurationId);
|
|
||||||
if (currentEnv == nextEnv) // Hint: This relies on the record equality to check if anything relevant has changed (e.g. server URL or ETag).
|
|
||||||
{
|
|
||||||
logger.LogInformation("Enterprise configuration '{ConfigId}' has not changed. No update required.", nextEnv.ConfigurationId);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var isNew = !currentIds.Contains(nextEnv.ConfigurationId);
|
reachableEnvironments.Add(config with { ETag = etagResponse.ETag });
|
||||||
if(isNew)
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Step 3: Compare with current environments and process changes.
|
||||||
|
// Download per configuration. A single failure must not block others.
|
||||||
|
//
|
||||||
|
var shouldDeferStartupDownloads = isFirstRun && !PluginFactory.IsInitialized;
|
||||||
|
var effectiveEnvironmentsById = new Dictionary<Guid, EnterpriseEnvironment>();
|
||||||
|
|
||||||
|
// Process new or changed configs:
|
||||||
|
foreach (var nextEnv in reachableEnvironments)
|
||||||
|
{
|
||||||
|
var hasCurrentEnvironment = currentEnvironmentsById.TryGetValue(nextEnv.ConfigurationId, out var currentEnv);
|
||||||
|
if (hasCurrentEnvironment && currentEnv == nextEnv) // Hint: This relies on the record equality to check if anything relevant has changed (e.g. server URL or ETag).
|
||||||
|
{
|
||||||
|
logger.LogInformation("Enterprise configuration '{ConfigId}' has not changed. No update required.", nextEnv.ConfigurationId);
|
||||||
|
effectiveEnvironmentsById[nextEnv.ConfigurationId] = nextEnv;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!hasCurrentEnvironment)
|
||||||
logger.LogInformation("Detected new enterprise configuration with ID '{ConfigId}' and server URL '{ServerUrl}'.", nextEnv.ConfigurationId, nextEnv.ConfigurationServerUrl);
|
logger.LogInformation("Detected new enterprise configuration with ID '{ConfigId}' and server URL '{ServerUrl}'.", nextEnv.ConfigurationId, nextEnv.ConfigurationServerUrl);
|
||||||
else
|
else
|
||||||
logger.LogInformation("Detected change in enterprise configuration with ID '{ConfigId}'. Server URL or ETag has changed.", nextEnv.ConfigurationId);
|
logger.LogInformation("Detected change in enterprise configuration with ID '{ConfigId}'. Server URL or ETag has changed.", nextEnv.ConfigurationId);
|
||||||
|
|
||||||
if (isFirstRun)
|
if (shouldDeferStartupDownloads)
|
||||||
|
{
|
||||||
MessageBus.INSTANCE.DeferMessage(null, Event.STARTUP_ENTERPRISE_ENVIRONMENT, nextEnv);
|
MessageBus.INSTANCE.DeferMessage(null, Event.STARTUP_ENTERPRISE_ENVIRONMENT, nextEnv);
|
||||||
|
effectiveEnvironmentsById[nextEnv.ConfigurationId] = nextEnv;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
await PluginFactory.TryDownloadingConfigPluginAsync(nextEnv.ConfigurationId, nextEnv.ConfigurationServerUrl);
|
{
|
||||||
|
var wasDownloadSuccessful = await PluginFactory.TryDownloadingConfigPluginAsync(nextEnv.ConfigurationId, nextEnv.ConfigurationServerUrl);
|
||||||
|
if (!wasDownloadSuccessful)
|
||||||
|
{
|
||||||
|
failedConfigIds.Add(nextEnv.ConfigurationId);
|
||||||
|
if (hasCurrentEnvironment)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Failed to update enterprise configuration '{ConfigId}'. Keeping the previously active version.", nextEnv.ConfigurationId);
|
||||||
|
effectiveEnvironmentsById[nextEnv.ConfigurationId] = currentEnv;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogWarning("Failed to download the new enterprise configuration '{ConfigId}'. Skipping activation for now.", nextEnv.ConfigurationId);
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
CURRENT_ENVIRONMENTS = nextEnvironments;
|
effectiveEnvironmentsById[nextEnv.ConfigurationId] = nextEnv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retain configurations for all failed IDs. On cold start there might be no
|
||||||
|
// previous in-memory snapshot yet, so we also keep the current fetched entry
|
||||||
|
// to protect it from cleanup while the server is unreachable.
|
||||||
|
foreach (var failedConfigId in failedConfigIds)
|
||||||
|
{
|
||||||
|
if (effectiveEnvironmentsById.ContainsKey(failedConfigId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!currentEnvironmentsById.TryGetValue(failedConfigId, out var retainedEnvironment))
|
||||||
|
{
|
||||||
|
if (!activeFetchedEnvironmentsById.TryGetValue(failedConfigId, out retainedEnvironment))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
logger.LogWarning("Could not refresh enterprise configuration '{ConfigId}'. Protecting it from cleanup until connectivity is restored.", failedConfigId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
logger.LogWarning("Could not refresh enterprise configuration '{ConfigId}'. Keeping the previously active version.", failedConfigId);
|
||||||
|
|
||||||
|
effectiveEnvironmentsById[failedConfigId] = retainedEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
var effectiveEnvironments = effectiveEnvironmentsById.Values.ToList();
|
||||||
|
|
||||||
|
// Cleanup is only allowed after a successful sync cycle:
|
||||||
|
if (PluginFactory.IsInitialized && !shouldDeferStartupDownloads)
|
||||||
|
PluginFactory.RemoveUnreferencedManagedConfigurationPlugins(effectiveEnvironmentsById.Keys.ToHashSet());
|
||||||
|
|
||||||
|
if (effectiveEnvironments.Count == 0)
|
||||||
|
logger.LogInformation("AI Studio runs without any enterprise configurations.");
|
||||||
|
|
||||||
|
CURRENT_ENVIRONMENTS = effectiveEnvironments;
|
||||||
|
HasValidEnterpriseSnapshot = true;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -100,7 +100,7 @@ public sealed class RustAvailabilityMonitorService : BackgroundService, IMessage
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await this.rustService.ReadUserLanguage();
|
await this.rustService.ReadUserLanguage(forceRequest: true);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -27,7 +27,11 @@ public sealed partial class RustService
|
|||||||
if (!secret.Success && !isTrying)
|
if (!secret.Success && !isTrying)
|
||||||
this.logger!.LogError($"Failed to get the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key': '{secret.Issue}'");
|
this.logger!.LogError($"Failed to get the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key': '{secret.Issue}'");
|
||||||
|
|
||||||
|
if (secret.Success)
|
||||||
this.logger!.LogDebug($"Successfully retrieved the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key'.");
|
this.logger!.LogDebug($"Successfully retrieved the API key for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key'.");
|
||||||
|
else if (isTrying)
|
||||||
|
this.logger!.LogDebug($"No API key configured for '{prefix}::{secretId.SecretId}::{secretId.SecretName}::api_key' (try mode): '{secret.Issue}'");
|
||||||
|
|
||||||
return secret;
|
return secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,13 +36,12 @@ public sealed partial class RustService
|
|||||||
var result = await this.http.GetAsync("/system/enterprise/configs");
|
var result = await this.http.GetAsync("/system/enterprise/configs");
|
||||||
if (!result.IsSuccessStatusCode)
|
if (!result.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
this.logger!.LogError($"Failed to query the enterprise configurations: '{result.StatusCode}'");
|
throw new HttpRequestException($"Failed to query the enterprise configurations: '{result.StatusCode}'");
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var configs = await result.Content.ReadFromJsonAsync<List<EnterpriseConfig>>(this.jsonRustSerializerOptions);
|
var configs = await result.Content.ReadFromJsonAsync<List<EnterpriseConfig>>(this.jsonRustSerializerOptions);
|
||||||
if (configs is null)
|
if (configs is null)
|
||||||
return [];
|
throw new InvalidOperationException("Failed to parse the enterprise configurations from Rust.");
|
||||||
|
|
||||||
var environments = new List<EnterpriseEnvironment>();
|
var environments = new List<EnterpriseEnvironment>();
|
||||||
foreach (var config in configs)
|
foreach (var config in configs)
|
||||||
@ -55,35 +54,4 @@ public sealed partial class RustService
|
|||||||
|
|
||||||
return environments;
|
return environments;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads all enterprise configuration IDs that should be deleted.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>
|
|
||||||
/// Returns a list of GUIDs representing configuration IDs to remove.
|
|
||||||
/// </returns>
|
|
||||||
public async Task<List<Guid>> EnterpriseEnvDeleteConfigIds()
|
|
||||||
{
|
|
||||||
var result = await this.http.GetAsync("/system/enterprise/delete-configs");
|
|
||||||
if (!result.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
this.logger!.LogError($"Failed to query the enterprise delete configuration IDs: '{result.StatusCode}'");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var ids = await result.Content.ReadFromJsonAsync<List<string>>(this.jsonRustSerializerOptions);
|
|
||||||
if (ids is null)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
var guids = new List<Guid>();
|
|
||||||
foreach (var idStr in ids)
|
|
||||||
{
|
|
||||||
if (Guid.TryParse(idStr, out var id))
|
|
||||||
guids.Add(id);
|
|
||||||
else
|
|
||||||
this.logger!.LogWarning($"Skipping invalid GUID in enterprise delete config IDs: '{idStr}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return guids;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -2,8 +2,17 @@
|
|||||||
|
|
||||||
public sealed partial class RustService
|
public sealed partial class RustService
|
||||||
{
|
{
|
||||||
public async Task<string> ReadUserLanguage()
|
public async Task<string> ReadUserLanguage(bool forceRequest = false)
|
||||||
{
|
{
|
||||||
|
if (!forceRequest && !string.IsNullOrWhiteSpace(this.cachedUserLanguage))
|
||||||
|
return this.cachedUserLanguage;
|
||||||
|
|
||||||
|
await this.userLanguageLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!forceRequest && !string.IsNullOrWhiteSpace(this.cachedUserLanguage))
|
||||||
|
return this.cachedUserLanguage;
|
||||||
|
|
||||||
var response = await this.http.GetAsync("/system/language");
|
var response = await this.http.GetAsync("/system/language");
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@ -11,6 +20,16 @@ public sealed partial class RustService
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.Content.ReadAsStringAsync();
|
var userLanguage = (await response.Content.ReadAsStringAsync()).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(userLanguage))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
this.cachedUserLanguage = userLanguage;
|
||||||
|
return userLanguage;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.userLanguageLock.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,6 +17,7 @@ public sealed partial class RustService : BackgroundService
|
|||||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(RustService).Namespace, nameof(RustService));
|
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(RustService).Namespace, nameof(RustService));
|
||||||
|
|
||||||
private readonly HttpClient http;
|
private readonly HttpClient http;
|
||||||
|
private readonly SemaphoreSlim userLanguageLock = new(1, 1);
|
||||||
|
|
||||||
private readonly JsonSerializerOptions jsonRustSerializerOptions = new()
|
private readonly JsonSerializerOptions jsonRustSerializerOptions = new()
|
||||||
{
|
{
|
||||||
@ -29,6 +30,7 @@ public sealed partial class RustService : BackgroundService
|
|||||||
|
|
||||||
private ILogger<RustService>? logger;
|
private ILogger<RustService>? logger;
|
||||||
private Encryption? encryptor;
|
private Encryption? encryptor;
|
||||||
|
private string? cachedUserLanguage;
|
||||||
|
|
||||||
private readonly string apiPort;
|
private readonly string apiPort;
|
||||||
private readonly string certificateFingerprint;
|
private readonly string certificateFingerprint;
|
||||||
@ -88,6 +90,7 @@ public sealed partial class RustService : BackgroundService
|
|||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
this.http.Dispose();
|
this.http.Dispose();
|
||||||
|
this.userLanguageLock.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,12 @@ public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME)
|
|||||||
|
|
||||||
private static RustService? RUST_SERVICE;
|
private static RustService? RUST_SERVICE;
|
||||||
|
|
||||||
|
// ReSharper disable FieldCanBeMadeReadOnly.Local
|
||||||
|
// ReSharper disable ConvertToConstant.Local
|
||||||
|
private static bool LOG_TO_STDOUT = true;
|
||||||
|
// ReSharper restore ConvertToConstant.Local
|
||||||
|
// ReSharper restore FieldCanBeMadeReadOnly.Local
|
||||||
|
|
||||||
// Buffer for early log events before the RustService is available:
|
// Buffer for early log events before the RustService is available:
|
||||||
private static readonly ConcurrentQueue<LogEventRequest> EARLY_LOG_BUFFER = new();
|
private static readonly ConcurrentQueue<LogEventRequest> EARLY_LOG_BUFFER = new();
|
||||||
|
|
||||||
@ -44,6 +50,10 @@ public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME)
|
|||||||
bufferedEvent.StackTrace
|
bufferedEvent.StackTrace
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !DEBUG
|
||||||
|
LOG_TO_STDOUT = false;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
|
public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
|
||||||
@ -56,6 +66,8 @@ public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME)
|
|||||||
var stackTrace = logEntry.Exception?.StackTrace;
|
var stackTrace = logEntry.Exception?.StackTrace;
|
||||||
var colorCode = GetColorForLogLevel(logEntry.LogLevel);
|
var colorCode = GetColorForLogLevel(logEntry.LogLevel);
|
||||||
|
|
||||||
|
if (LOG_TO_STDOUT)
|
||||||
|
{
|
||||||
textWriter.Write($"[{colorCode}{timestamp}{ANSI_RESET}] {colorCode}{logLevel}{ANSI_RESET} [{category}] {colorCode}{message}{ANSI_RESET}");
|
textWriter.Write($"[{colorCode}{timestamp}{ANSI_RESET}] {colorCode}{logLevel}{ANSI_RESET} [{category}] {colorCode}{message}{ANSI_RESET}");
|
||||||
if (logEntry.Exception is not null)
|
if (logEntry.Exception is not null)
|
||||||
{
|
{
|
||||||
@ -69,6 +81,7 @@ public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
textWriter.WriteLine();
|
textWriter.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
// Send log event to Rust via API (fire-and-forget):
|
// Send log event to Rust via API (fire-and-forget):
|
||||||
if (RUST_SERVICE is not null)
|
if (RUST_SERVICE is not null)
|
||||||
|
|||||||
@ -37,9 +37,9 @@
|
|||||||
},
|
},
|
||||||
"Microsoft.NET.ILLink.Tasks": {
|
"Microsoft.NET.ILLink.Tasks": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[9.0.12, )",
|
"requested": "[9.0.13, )",
|
||||||
"resolved": "9.0.12",
|
"resolved": "9.0.13",
|
||||||
"contentHash": "StA3kyImQHqDo8A8ZHaSxgASbEuT5UIqgeCvK5SzUPj//xE1QSys421J9pEs4cYuIVwq7CJvWSKxtyH7aPr1LA=="
|
"contentHash": "f7t15I9ZXV7fNk3FIzPAlkJNG1A1tkSeDpRh+TFWEToGGqA+uj6uqU15I8YOkkYICNY2tqOVm2CMe6ScPFPwEg=="
|
||||||
},
|
},
|
||||||
"MudBlazor": {
|
"MudBlazor": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
@ -64,9 +64,9 @@
|
|||||||
},
|
},
|
||||||
"Qdrant.Client": {
|
"Qdrant.Client": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[1.16.1, )",
|
"requested": "[1.17.0, )",
|
||||||
"resolved": "1.16.1",
|
"resolved": "1.17.0",
|
||||||
"contentHash": "EJo50JXTdjY2JOUphCFLXoHukI/tz/ykLCmMnQHUjsKT22ZfL0XIdEziHOC3vjw2SOoY8WDVQ+AxixEonejOZA==",
|
"contentHash": "QFNtVu4Kiz6NHAAi2UQk+Ia64/qyX1NMecQGIBGnKqFOlpnxI3OCCBRBKXWGPk/c+4vAmR3Dj+cQ9apqX0zU8A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Google.Protobuf": "3.31.0",
|
"Google.Protobuf": "3.31.0",
|
||||||
"Grpc.Net.Client": "2.71.0"
|
"Grpc.Net.Client": "2.71.0"
|
||||||
|
|||||||
@ -1,14 +1,24 @@
|
|||||||
# v26.2.2, build 234 (2026-02-xx xx:xx UTC)
|
# v26.2.2, build 234 (2026-02-22 14:14 UTC)
|
||||||
- Added a vector database (Qdrant) as a building block for our local RAG (retrieval-augmented generation) solution. Thank you very much, Paul (`PaulKoudelka`), for this major contribution. Note that our local RAG implementation remained in preview and has not yet been released; other building blocks are not yet ready.
|
- Added a vector database (Qdrant) as a building block for our local RAG (retrieval-augmented generation) solution. Thank you very much, Paul (`PaulKoudelka`), for this major contribution. Note that our local RAG implementation remained in preview and has not yet been released; other building blocks are not yet ready.
|
||||||
|
- Added an option in the embedding providers table to test the embedding process.
|
||||||
- Added an app setting to enable administration options for IT staff to configure and maintain organization-wide settings.
|
- Added an app setting to enable administration options for IT staff to configure and maintain organization-wide settings.
|
||||||
- Added an option to export all provider types (LLMs, embeddings, transcriptions) so you can use them in a configuration plugin. You'll be asked if you want to export the related API key too. API keys will be encrypted in the export. This feature only shows up when administration options are enabled.
|
- Added an option to export all provider types (LLMs, embeddings, transcriptions) so you can use them in a configuration plugin. You'll be asked if you want to export the related API key too. API keys will be encrypted in the export. This feature only shows up when administration options are enabled.
|
||||||
- Added an option in the app settings to create an encryption secret, which is required to encrypt values (for example, API keys) in configuration plugins. This feature only shows up when administration options are enabled.
|
- Added an option in the app settings to create an encryption secret, which is required to encrypt values (for example, API keys) in configuration plugins. This feature only shows up when administration options are enabled.
|
||||||
|
- Added the option to set a predefined provider for the entire app via configuration plugins.
|
||||||
- Added support for using multiple enterprise configurations simultaneously. Enabled organizations to apply configurations based on employee affiliations, such as departments and working groups. See the enterprise configuration documentation for details.
|
- Added support for using multiple enterprise configurations simultaneously. Enabled organizations to apply configurations based on employee affiliations, such as departments and working groups. See the enterprise configuration documentation for details.
|
||||||
|
- Added the `DEPLOYED_USING_CONFIG_SERVER` field for configuration plugins so enterprise-managed plugins can be identified explicitly. Administrators should update their configuration plugins accordingly. See the enterprise configuration documentation for details.
|
||||||
|
- Improved the enterprise configuration synchronization to be fail-safe on unstable or unavailable internet connections (for example, during business travel). If metadata checks or downloads fail, AI Studio keeps the current configuration plugins unchanged.
|
||||||
- Improved the document analysis assistant (in beta) by hiding the export functionality by default. Enable the administration options in the app settings to show and use the export functionality. This streamlines the usage for regular users.
|
- Improved the document analysis assistant (in beta) by hiding the export functionality by default. Enable the administration options in the app settings to show and use the export functionality. This streamlines the usage for regular users.
|
||||||
- Improved the workspaces experience by using a different color for the delete button to avoid confusion.
|
- Improved the workspaces experience by using a different color for the delete button to avoid confusion.
|
||||||
- Improved single-input dialogs (e.g., renaming chats) so pressing `Enter` confirmed immediately and the input field focused automatically when the dialog opened.
|
- Improved single-input dialogs (e.g., renaming chats) so pressing `Enter` confirmed immediately and the input field focused automatically when the dialog opened.
|
||||||
- Improved the plugins page by adding an action to open the plugin source link. The action opens website URLs in an external browser, supports `mailto:` links for direct email composition.
|
- Improved the plugins page by adding an action to open the plugin source link. The action opens website URLs in an external browser, supports `mailto:` links for direct email composition.
|
||||||
|
- Improved the configuration plugins by making `EnabledPreviewFeatures` additive rather than exclusive. Users can now enable additional preview features without being restricted to those selected by the configuration plugin.
|
||||||
- Improved the system language detection for locale values such as `C` and variants like `de_DE.UTF-8`, enabling AI Studio to apply the matching UI language more reliably.
|
- Improved the system language detection for locale values such as `C` and variants like `de_DE.UTF-8`, enabling AI Studio to apply the matching UI language more reliably.
|
||||||
|
- Fixed an issue where leftover enterprise configuration plugins could remain active after organizational assignment changes during longer absences (for example, vacation), which could lead to configuration conflicts.
|
||||||
- Fixed an issue where manually saving chats in workspace manual-storage mode could appear unreliable during response streaming. The save button is now disabled while streaming to prevent partial saves.
|
- Fixed an issue where manually saving chats in workspace manual-storage mode could appear unreliable during response streaming. The save button is now disabled while streaming to prevent partial saves.
|
||||||
|
- Fixed an issue where in some places "No profile" was displayed instead of the localized text.
|
||||||
- Fixed a bug in the Responses API of our OpenAI provider implementation where streamed whitespace chunks were discarded. We thank Oliver Kunc `OliverKunc` for his first contribution in resolving this issue. We appreciate your help, Oliver.
|
- Fixed a bug in the Responses API of our OpenAI provider implementation where streamed whitespace chunks were discarded. We thank Oliver Kunc `OliverKunc` for his first contribution in resolving this issue. We appreciate your help, Oliver.
|
||||||
|
- Fixed a bug in the Microsoft Word export via Pandoc where target paths containing spaces or Unicode characters were split into invalid command arguments, causing export failures. Thanks to Bernhard for reporting this issue.
|
||||||
|
- Fixed the Google Gemini model API. Switched to the default OpenAI-compatible API to retrieve the model list after Google changed the previous API, which stopped working.
|
||||||
|
- Upgraded to .NET 9.0.13 & Rust 1.93.1.
|
||||||
- Upgraded dependencies.
|
- Upgraded dependencies.
|
||||||
7
app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md
Normal file
7
app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# v26.3.1, build 235 (2026-03-xx xx:xx UTC)
|
||||||
|
- Improved the performance by caching the OS language detection and requesting the user language only once per app start.
|
||||||
|
- Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations.
|
||||||
|
- Improved the user-language logging by limiting language detection logs to a single entry per app start.
|
||||||
|
- Improved the logbook readability by removing non-readable special characters from log entries.
|
||||||
|
- Improved the logbook reliability by significantly reducing duplicate log entries.
|
||||||
|
- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. Thanks Inga for reporting this issue and providing some context on how to reproduce it.
|
||||||
@ -25,8 +25,6 @@ AI Studio supports loading multiple enterprise configurations simultaneously. Th
|
|||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `configs` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS`: A combined format containing one or more configuration entries. Each entry consists of a configuration ID and a server URL separated by `@`. Multiple entries are separated by `;`. The format is: `id1@url1;id2@url2;id3@url3`. The configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier).
|
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `configs` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS`: A combined format containing one or more configuration entries. Each entry consists of a configuration ID and a server URL separated by `@`. Multiple entries are separated by `;`. The format is: `id1@url1;id2@url2;id3@url3`. The configuration ID must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier).
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `delete_config_ids` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_DELETE_CONFIG_IDS`: One or more configuration IDs that should be removed, separated by `;`. The format is: `id1;id2;id3`. This is helpful if an employee moves to a different department or leaves the organization.
|
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_encryption_secret` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET`: A base64-encoded 32-byte encryption key for decrypting API keys in configuration plugins. This is optional and only needed if you want to include encrypted API keys in your configuration. All configurations share the same encryption secret.
|
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_encryption_secret` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET`: A base64-encoded 32-byte encryption key for decrypting API keys in configuration plugins. This is optional and only needed if you want to include encrypted API keys in your configuration. All configurations share the same encryption secret.
|
||||||
|
|
||||||
**Example:** To configure two enterprise configurations (one for the organization and one for a department):
|
**Example:** To configure two enterprise configurations (one for the organization and one for a department):
|
||||||
@ -37,14 +35,100 @@ MINDWORK_AI_STUDIO_ENTERPRISE_CONFIGS=9072b77d-ca81-40da-be6a-861da525ef7b@https
|
|||||||
|
|
||||||
**Priority:** When multiple configurations define the same setting (e.g., a provider with the same ID), the first definition wins. The order of entries in the variable determines priority. Place the organization-wide configuration first, followed by department-specific configurations if the organization should have higher priority.
|
**Priority:** When multiple configurations define the same setting (e.g., a provider with the same ID), the first definition wins. The order of entries in the variable determines priority. Place the organization-wide configuration first, followed by department-specific configurations if the organization should have higher priority.
|
||||||
|
|
||||||
|
### Windows GPO / PowerShell example for `configs`
|
||||||
|
|
||||||
|
If you distribute multiple GPOs, each GPO should read and write the same registry value (`configs`) and only update its own `id@url` entry. Other entries must stay untouched.
|
||||||
|
|
||||||
|
The following PowerShell example provides helper functions for appending and removing entries safely:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$RegistryPath = "HKCU:\Software\github\MindWork AI Studio\Enterprise IT"
|
||||||
|
$ConfigsValueName = "configs"
|
||||||
|
|
||||||
|
function Get-ConfigEntries {
|
||||||
|
param([string]$RawValue)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($RawValue)) { return @() }
|
||||||
|
|
||||||
|
$entries = @()
|
||||||
|
foreach ($part in $RawValue.Split(';')) {
|
||||||
|
$trimmed = $part.Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($trimmed)) { continue }
|
||||||
|
|
||||||
|
$pair = $trimmed.Split('@', 2)
|
||||||
|
if ($pair.Count -ne 2) { continue }
|
||||||
|
|
||||||
|
$id = $pair[0].Trim().ToLowerInvariant()
|
||||||
|
$url = $pair[1].Trim()
|
||||||
|
if ([string]::IsNullOrWhiteSpace($id) -or [string]::IsNullOrWhiteSpace($url)) { continue }
|
||||||
|
|
||||||
|
$entries += [PSCustomObject]@{
|
||||||
|
Id = $id
|
||||||
|
Url = $url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entries
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-ConfigValue {
|
||||||
|
param([array]$Entries)
|
||||||
|
|
||||||
|
return ($Entries | ForEach-Object { "$($_.Id)@$($_.Url)" }) -join ';'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-EnterpriseConfigEntry {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][Guid]$ConfigId,
|
||||||
|
[Parameter(Mandatory=$true)][string]$ServerUrl
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path $RegistryPath)) {
|
||||||
|
New-Item -Path $RegistryPath -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = (Get-ItemProperty -Path $RegistryPath -Name $ConfigsValueName -ErrorAction SilentlyContinue).$ConfigsValueName
|
||||||
|
$entries = Get-ConfigEntries -RawValue $raw
|
||||||
|
$normalizedId = $ConfigId.ToString().ToLowerInvariant()
|
||||||
|
$normalizedUrl = $ServerUrl.Trim()
|
||||||
|
|
||||||
|
# Replace only this one ID, keep all other entries unchanged.
|
||||||
|
$entries = @($entries | Where-Object { $_.Id -ne $normalizedId })
|
||||||
|
$entries += [PSCustomObject]@{
|
||||||
|
Id = $normalizedId
|
||||||
|
Url = $normalizedUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
Set-ItemProperty -Path $RegistryPath -Name $ConfigsValueName -Type String -Value (ConvertTo-ConfigValue -Entries $entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-EnterpriseConfigEntry {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)][Guid]$ConfigId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-Path $RegistryPath)) { return }
|
||||||
|
|
||||||
|
$raw = (Get-ItemProperty -Path $RegistryPath -Name $ConfigsValueName -ErrorAction SilentlyContinue).$ConfigsValueName
|
||||||
|
$entries = Get-ConfigEntries -RawValue $raw
|
||||||
|
$normalizedId = $ConfigId.ToString().ToLowerInvariant()
|
||||||
|
|
||||||
|
# Remove only this one ID, keep all other entries unchanged.
|
||||||
|
$updated = @($entries | Where-Object { $_.Id -ne $normalizedId })
|
||||||
|
Set-ItemProperty -Path $RegistryPath -Name $ConfigsValueName -Type String -Value (ConvertTo-ConfigValue -Entries $updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Example usage:
|
||||||
|
# Add-EnterpriseConfigEntry -ConfigId "9072b77d-ca81-40da-be6a-861da525ef7b" -ServerUrl "https://intranet.example.org:30100/ai-studio/configuration"
|
||||||
|
# Remove-EnterpriseConfigEntry -ConfigId "9072b77d-ca81-40da-be6a-861da525ef7b"
|
||||||
|
```
|
||||||
|
|
||||||
### Single configuration (legacy)
|
### Single configuration (legacy)
|
||||||
|
|
||||||
The following single-configuration keys and variables are still supported for backwards compatibility. AI Studio always reads both the multi-config and legacy variables and merges all found configurations into one list. If a configuration ID appears in both, the entry from the multi-config format takes priority (first occurrence wins). This means you can migrate to the new format incrementally without losing existing configurations:
|
The following single-configuration keys and variables are still supported for backwards compatibility. AI Studio always reads both the multi-config and legacy variables and merges all found configurations into one list. If a configuration ID appears in both, the entry from the multi-config format takes priority (first occurrence wins). This means you can migrate to the new format incrementally without losing existing configurations:
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_id` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID`: This must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). It uniquely identifies the configuration. You can use an ID per department, institute, or even per person.
|
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_id` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ID`: This must be a valid [GUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Globally_unique_identifier). It uniquely identifies the configuration. You can use an ID per department, institute, or even per person.
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `delete_config_id` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_DELETE_CONFIG_ID`: This is a configuration ID that should be removed. This is helpful if an employee moves to a different department or leaves the organization.
|
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_server_url` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL`: An HTTP or HTTPS address using an IP address or DNS name. This is the web server from which AI Studio attempts to load the specified configuration as a ZIP file.
|
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_server_url` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_SERVER_URL`: An HTTP or HTTPS address using an IP address or DNS name. This is the web server from which AI Studio attempts to load the specified configuration as a ZIP file.
|
||||||
|
|
||||||
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_encryption_secret` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET`: A base64-encoded 32-byte encryption key for decrypting API keys in configuration plugins. This is optional and only needed if you want to include encrypted API keys in your configuration.
|
- Key `HKEY_CURRENT_USER\Software\github\MindWork AI Studio\Enterprise IT`, value `config_encryption_secret` or variable `MINDWORK_AI_STUDIO_ENTERPRISE_CONFIG_ENCRYPTION_SECRET`: A base64-encoded 32-byte encryption key for decrypting API keys in configuration plugins. This is optional and only needed if you want to include encrypted API keys in your configuration.
|
||||||
@ -107,6 +191,16 @@ For example, if your enterprise configuration ID is `9072b77d-ca81-40da-be6a-861
|
|||||||
ID = "9072b77d-ca81-40da-be6a-861da525ef7b"
|
ID = "9072b77d-ca81-40da-be6a-861da525ef7b"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Important: Mark enterprise-managed plugins explicitly
|
||||||
|
|
||||||
|
Configuration plugins deployed by your configuration server should define:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
DEPLOYED_USING_CONFIG_SERVER = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Local, manually managed configuration plugins should set this to `false`. If the field is missing, AI Studio falls back to the plugin path (`.config`) to determine whether the plugin is managed and logs a warning.
|
||||||
|
|
||||||
## Example AI Studio configuration
|
## Example AI Studio configuration
|
||||||
The latest example of an AI Studio configuration via configuration plugin can always be found in the repository in the `app/MindWork AI Studio/Plugins/configuration` folder. Here are the links to the files:
|
The latest example of an AI Studio configuration via configuration plugin can always be found in the repository in the `app/MindWork AI Studio/Plugins/configuration` folder. Here are the links to the files:
|
||||||
|
|
||||||
|
|||||||
16
metadata.txt
16
metadata.txt
@ -1,12 +1,12 @@
|
|||||||
26.2.1
|
26.2.2
|
||||||
2026-02-01 19:16:01 UTC
|
2026-02-22 14:14:47 UTC
|
||||||
233
|
234
|
||||||
9.0.113 (commit 64f9f590b3)
|
9.0.114 (commit 4c5aac3d56)
|
||||||
9.0.12 (commit 2f12400757)
|
9.0.13 (commit 9ecbfd4f3f)
|
||||||
1.93.0 (commit 254b59607)
|
1.93.1 (commit 01f6ddf75)
|
||||||
8.15.0
|
8.15.0
|
||||||
1.8.1
|
1.8.1
|
||||||
8f9cd40d060, release
|
3eb367d4c9e, release
|
||||||
osx-arm64
|
osx-arm64
|
||||||
144.0.7543.0
|
144.0.7543.0
|
||||||
1.16.3
|
1.17.0
|
||||||
2
runtime/Cargo.lock
generated
2
runtime/Cargo.lock
generated
@ -2789,7 +2789,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mindwork-ai-studio"
|
name = "mindwork-ai-studio"
|
||||||
version = "26.2.1"
|
version = "26.2.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"arboard",
|
"arboard",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mindwork-ai-studio"
|
name = "mindwork-ai-studio"
|
||||||
version = "26.2.1"
|
version = "26.2.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "MindWork AI Studio"
|
description = "MindWork AI Studio"
|
||||||
authors = ["Thorsten Sommer"]
|
authors = ["Thorsten Sommer"]
|
||||||
|
|||||||
@ -33,6 +33,59 @@ static DOTNET_INITIALIZED: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
|
|||||||
pub const PID_FILE_NAME: &str = "mindwork_ai_studio.pid";
|
pub const PID_FILE_NAME: &str = "mindwork_ai_studio.pid";
|
||||||
const SIDECAR_TYPE:SidecarType = SidecarType::Dotnet;
|
const SIDECAR_TYPE:SidecarType = SidecarType::Dotnet;
|
||||||
|
|
||||||
|
/// Removes ANSI escape sequences and non-printable control chars from stdout lines.
|
||||||
|
fn sanitize_stdout_line(line: &str) -> String {
|
||||||
|
let mut sanitized = String::with_capacity(line.len());
|
||||||
|
let mut chars = line.chars().peekable();
|
||||||
|
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
if ch == '\u{1B}' {
|
||||||
|
if let Some(next) = chars.peek().copied() {
|
||||||
|
// CSI sequence: ESC [ ... <final>
|
||||||
|
if next == '[' {
|
||||||
|
chars.next();
|
||||||
|
for csi_char in chars.by_ref() {
|
||||||
|
let code = csi_char as u32;
|
||||||
|
if (0x40..=0x7E).contains(&code) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OSC sequence: ESC ] ... (BEL or ESC \)
|
||||||
|
if next == ']' {
|
||||||
|
chars.next();
|
||||||
|
let mut previous_was_escape = false;
|
||||||
|
for osc_char in chars.by_ref() {
|
||||||
|
if osc_char == '\u{07}' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if previous_was_escape && osc_char == '\\' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
previous_was_escape = osc_char == '\u{1B}';
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown escape sequence: ignore the escape char itself.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch.is_control() && ch != '\t' {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized.push(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the desired port of the .NET server. Our .NET app calls this endpoint to get
|
/// Returns the desired port of the .NET server. Our .NET app calls this endpoint to get
|
||||||
/// the port where the .NET server should listen to.
|
/// the port where the .NET server should listen to.
|
||||||
#[get("/system/dotnet/port")]
|
#[get("/system/dotnet/port")]
|
||||||
@ -111,11 +164,12 @@ pub fn start_dotnet_server() {
|
|||||||
// NOTE: Log events are sent via structured HTTP API calls.
|
// NOTE: Log events are sent via structured HTTP API calls.
|
||||||
// This loop serves for fundamental output (e.g., startup errors).
|
// This loop serves for fundamental output (e.g., startup errors).
|
||||||
while let Some(CommandEvent::Stdout(line)) = rx.recv().await {
|
while let Some(CommandEvent::Stdout(line)) = rx.recv().await {
|
||||||
let line = line.trim_end();
|
let line = sanitize_stdout_line(line.trim_end());
|
||||||
|
if !line.trim().is_empty() {
|
||||||
info!(Source = ".NET Server (stdout)"; "{line}");
|
info!(Source = ".NET Server (stdout)"; "{line}");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This endpoint is called by the .NET server to signal that the server is ready.
|
/// This endpoint is called by the .NET server to signal that the server is ready.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user