mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-09 23:53:48 +00:00
Added provider icons (#934)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
afdcc0455d
commit
e78a0e61b9
74 files changed
+1018
-74
No files matched your search
@@ -21,7 +21,8 @@ public sealed record EmbeddingProvider(
|
||||
Guid EnterpriseConfigurationPluginId = default,
|
||||
string Hostname = "http://localhost:1234",
|
||||
Host Host = Host.NONE,
|
||||
bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
|
||||
bool AllowUserProvidedAPIKey = false,
|
||||
string CustomIconDataUrl = "") : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
|
||||
{
|
||||
private static readonly ILogger<EmbeddingProvider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<EmbeddingProvider>();
|
||||
|
||||
@@ -53,7 +54,7 @@ public sealed record EmbeddingProvider(
|
||||
|
||||
#endregion
|
||||
|
||||
public static bool TryParseEmbeddingProviderTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject provider)
|
||||
public static bool TryParseEmbeddingProviderTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject provider)
|
||||
{
|
||||
provider = NONE;
|
||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
||||
@@ -102,6 +103,15 @@ public sealed record EmbeddingProvider(
|
||||
if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead<bool>(out var allowUserProvidedApiKeyBool))
|
||||
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
|
||||
|
||||
var customIconDataUrl = string.Empty;
|
||||
if (table.TryGetValue("IconPath", out var iconPathValue))
|
||||
{
|
||||
if (!iconPathValue.TryRead<string>(out var iconPath))
|
||||
LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid icon path. Falling back to the built-in provider icon. (Plugin ID: {configPluginId})");
|
||||
else if (!PluginIconFile.TryLoadDataUrl(iconPath, pluginPath, out customIconDataUrl, out var iconIssue))
|
||||
LOGGER.LogWarning($"The configured embedding provider {idx} contains an invalid icon path. Falling back to the built-in provider icon. Issue: {iconIssue} (Plugin ID: {configPluginId})");
|
||||
}
|
||||
|
||||
provider = new EmbeddingProvider
|
||||
{
|
||||
Num = 0, // will be set later by the PluginConfigurationObject
|
||||
@@ -115,6 +125,7 @@ public sealed record EmbeddingProvider(
|
||||
Hostname = hostname,
|
||||
Host = host,
|
||||
AllowUserProvidedAPIKey = allowUserProvidedApiKey,
|
||||
CustomIconDataUrl = customIconDataUrl,
|
||||
};
|
||||
|
||||
// Handle an encrypted API key if present. When the user manages their own key for this
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Loads an icon file a configuration plugin points to, such as a custom provider logo.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The checks here are about the path, not about the markup: they keep a plugin from turning an
|
||||
/// arbitrary file somewhere on the system into a data URL. Whether the file is a usable SVG, and
|
||||
/// how it becomes a data URL, is up to SvgIcon.
|
||||
/// </remarks>
|
||||
internal static class PluginIconFile
|
||||
{
|
||||
public static bool TryLoadDataUrl(string iconPath, string pluginPath, out string dataUrl, out string issue)
|
||||
{
|
||||
dataUrl = string.Empty;
|
||||
issue = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(iconPath))
|
||||
{
|
||||
issue = "The icon path is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Path.IsPathFullyQualified(iconPath))
|
||||
{
|
||||
issue = "The icon path must be relative to the configuration plugin directory.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pluginPath))
|
||||
{
|
||||
issue = "The icon path cannot be resolved because the configuration plugin directory is unknown.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var relativePath = iconPath
|
||||
.Replace('/', Path.DirectorySeparatorChar)
|
||||
.Replace('\\', Path.DirectorySeparatorChar);
|
||||
|
||||
if (relativePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Any(segment => segment == ".."))
|
||||
{
|
||||
issue = "The icon path must not contain '..' path segments.";
|
||||
return false;
|
||||
}
|
||||
|
||||
string pluginRoot;
|
||||
string resolvedPath;
|
||||
try
|
||||
{
|
||||
pluginRoot = Path.GetFullPath(pluginPath);
|
||||
resolvedPath = Path.GetFullPath(Path.Combine(pluginRoot, relativePath));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
issue = $"The icon path is invalid: {e.Message}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsInsideDirectory(pluginRoot, resolvedPath))
|
||||
{
|
||||
issue = "The icon path points outside of the configuration plugin directory.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(Path.GetExtension(resolvedPath), ".svg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
issue = "The icon file must use the .svg extension.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!File.Exists(resolvedPath))
|
||||
{
|
||||
issue = "The icon file does not exist.";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Check the size before reading, so an oversized file never reaches memory:
|
||||
var fileInfo = new FileInfo(resolvedPath);
|
||||
if (fileInfo.Length is <= 0 or > SvgIcon.MAX_ICON_SIZE_BYTES)
|
||||
{
|
||||
issue = $"The icon file must be between 1 byte and {SvgIcon.MAX_ICON_SIZE_BYTES / 1024} KiB.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LinksStayInsideDirectory(pluginRoot, resolvedPath))
|
||||
{
|
||||
issue = "The icon path contains a link that points outside of the configuration plugin directory.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return SvgIcon.TryCreateDataUrl(File.ReadAllBytes(resolvedPath), out dataUrl, out issue);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
issue = $"The icon file could not be read: {e.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool LinksStayInsideDirectory(string rootDirectory, string filePath)
|
||||
{
|
||||
var relativePath = Path.GetRelativePath(rootDirectory, filePath);
|
||||
var currentPath = Path.GetFullPath(rootDirectory);
|
||||
foreach (var segment in relativePath.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
currentPath = Path.Combine(currentPath, segment);
|
||||
FileSystemInfo pathInfo = Directory.Exists(currentPath) ? new DirectoryInfo(currentPath) : new FileInfo(currentPath);
|
||||
var finalTarget = pathInfo.ResolveLinkTarget(true);
|
||||
if (finalTarget is not null && !IsInsideDirectory(rootDirectory, finalTarget.FullName))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsInsideDirectory(string rootDirectory, string path)
|
||||
{
|
||||
var root = Path.GetFullPath(rootDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
var target = Path.GetFullPath(path);
|
||||
var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
return target.StartsWith(root, comparison);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ namespace AIStudio.Settings;
|
||||
/// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param>
|
||||
/// <param name="Model">The LLM model to use for chat.</param>
|
||||
/// <param name="AllowUserProvidedAPIKey">When set by a configuration plugin, the user may set their own API key for this otherwise locked, enterprise-managed provider.</param>
|
||||
/// <param name="CustomIconDataUrl">The validated custom SVG icon supplied by a configuration plugin.</param>
|
||||
public sealed record Provider(
|
||||
uint Num,
|
||||
string Id,
|
||||
@@ -36,7 +37,8 @@ public sealed record Provider(
|
||||
HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE,
|
||||
string AdditionalJsonApiParameters = "",
|
||||
ProviderCapabilityOverrides? CapabilityOverrides = null,
|
||||
bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
|
||||
bool AllowUserProvidedAPIKey = false,
|
||||
string CustomIconDataUrl = "") : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
|
||||
{
|
||||
private static readonly ILogger<Provider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Provider>();
|
||||
|
||||
@@ -93,7 +95,7 @@ public sealed record Provider(
|
||||
|
||||
#endregion
|
||||
|
||||
public static bool TryParseProviderTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject provider)
|
||||
public static bool TryParseProviderTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject provider)
|
||||
{
|
||||
provider = NONE;
|
||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
||||
@@ -161,6 +163,15 @@ public sealed record Provider(
|
||||
if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead<bool>(out var allowUserProvidedApiKeyBool))
|
||||
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
|
||||
|
||||
var customIconDataUrl = string.Empty;
|
||||
if (table.TryGetValue("IconPath", out var iconPathValue))
|
||||
{
|
||||
if (!iconPathValue.TryRead<string>(out var iconPath))
|
||||
LOGGER.LogWarning($"The configured provider {idx} does not contain a valid icon path. Falling back to the built-in provider icon. (Plugin ID: {configPluginId})");
|
||||
else if (!PluginIconFile.TryLoadDataUrl(iconPath, pluginPath, out customIconDataUrl, out var iconIssue))
|
||||
LOGGER.LogWarning($"The configured provider {idx} contains an invalid icon path. Falling back to the built-in provider icon. Issue: {iconIssue} (Plugin ID: {configPluginId})");
|
||||
}
|
||||
|
||||
provider = new Provider
|
||||
{
|
||||
Num = 0, // will be set later by the PluginConfigurationObject
|
||||
@@ -177,6 +188,7 @@ public sealed record Provider(
|
||||
AdditionalJsonApiParameters = additionalJsonApiParameters,
|
||||
CapabilityOverrides = capabilityOverrides,
|
||||
AllowUserProvidedAPIKey = allowUserProvidedApiKey,
|
||||
CustomIconDataUrl = customIconDataUrl,
|
||||
};
|
||||
|
||||
// Handle an encrypted API key if present. When the user manages their own key for this
|
||||
|
||||
@@ -660,6 +660,29 @@ public sealed class SettingsManager
|
||||
.ThenBy(x => x.Num)
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the embedding provider with the given id, without applying any confidence filtering.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method resolves a stored embedding provider reference by its id. It applies neither the
|
||||
/// global minimum confidence level nor any component-specific minimum, so it returns the
|
||||
/// requested embedding provider even when the user enforces a higher global minimum. Callers
|
||||
/// that intend to send data to the returned embedding provider must check it themselves, for
|
||||
/// example through IsTrustedForDataSourceSecurityChecks.
|
||||
/// </remarks>
|
||||
/// <param name="embeddingProviderId">The id of the embedding provider to look up.</param>
|
||||
/// <returns>The embedding provider, or EmbeddingProvider.NONE when no embedding provider with that id exists.</returns>
|
||||
public EmbeddingProvider GetEmbeddingProviderById(string? embeddingProviderId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(embeddingProviderId))
|
||||
return EmbeddingProvider.NONE;
|
||||
|
||||
if (string.Equals(embeddingProviderId, EmbeddingProvider.NONE.Id, StringComparison.OrdinalIgnoreCase))
|
||||
return EmbeddingProvider.NONE;
|
||||
|
||||
return this.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id.Equals(embeddingProviderId, StringComparison.OrdinalIgnoreCase)) ?? EmbeddingProvider.NONE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all configured transcription providers.
|
||||
/// </summary>
|
||||
@@ -675,6 +698,29 @@ public sealed class SettingsManager
|
||||
.ThenBy(x => x.Num)
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the transcription provider with the given id, without applying any confidence filtering.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method resolves a stored transcription provider reference by its id. It applies neither
|
||||
/// the global minimum confidence level nor any component-specific minimum, so it returns the
|
||||
/// requested transcription provider even when the user enforces a higher global minimum. Callers
|
||||
/// that intend to send audio to the returned transcription provider must check its confidence
|
||||
/// level themselves, the way GetFilteredTranscriptionProviders does for the app settings.
|
||||
/// </remarks>
|
||||
/// <param name="transcriptionProviderId">The id of the transcription provider to look up.</param>
|
||||
/// <returns>The transcription provider, or TranscriptionProvider.NONE when no transcription provider with that id exists.</returns>
|
||||
public TranscriptionProvider GetTranscriptionProviderById(string? transcriptionProviderId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(transcriptionProviderId))
|
||||
return TranscriptionProvider.NONE;
|
||||
|
||||
if (string.Equals(transcriptionProviderId, TranscriptionProvider.NONE.Id, StringComparison.OrdinalIgnoreCase))
|
||||
return TranscriptionProvider.NONE;
|
||||
|
||||
return this.ConfigurationData.TranscriptionProviders.FirstOrDefault(x => x.Id.Equals(transcriptionProviderId, StringComparison.OrdinalIgnoreCase)) ?? TranscriptionProvider.NONE;
|
||||
}
|
||||
|
||||
public Profile GetPreselectedProfile(Tools.Components component)
|
||||
{
|
||||
var preselection = component.GetProfilePreselection(this);
|
||||
|
||||
@@ -21,7 +21,8 @@ public sealed record TranscriptionProvider(
|
||||
Guid EnterpriseConfigurationPluginId = default,
|
||||
string Hostname = "http://localhost:1234",
|
||||
Host Host = Host.NONE,
|
||||
bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
|
||||
bool AllowUserProvidedAPIKey = false,
|
||||
string CustomIconDataUrl = "") : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey
|
||||
{
|
||||
private static readonly ILogger<TranscriptionProvider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<TranscriptionProvider>();
|
||||
|
||||
@@ -53,7 +54,7 @@ public sealed record TranscriptionProvider(
|
||||
|
||||
#endregion
|
||||
|
||||
public static bool TryParseTranscriptionProviderTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject provider)
|
||||
public static bool TryParseTranscriptionProviderTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject provider)
|
||||
{
|
||||
provider = NONE;
|
||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
||||
@@ -102,6 +103,15 @@ public sealed record TranscriptionProvider(
|
||||
if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead<bool>(out var allowUserProvidedApiKeyBool))
|
||||
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
|
||||
|
||||
var customIconDataUrl = string.Empty;
|
||||
if (table.TryGetValue("IconPath", out var iconPathValue))
|
||||
{
|
||||
if (!iconPathValue.TryRead<string>(out var iconPath))
|
||||
LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid icon path. Falling back to the built-in provider icon. (Plugin ID: {configPluginId})");
|
||||
else if (!PluginIconFile.TryLoadDataUrl(iconPath, pluginPath, out customIconDataUrl, out var iconIssue))
|
||||
LOGGER.LogWarning($"The configured transcription provider {idx} contains an invalid icon path. Falling back to the built-in provider icon. Issue: {iconIssue} (Plugin ID: {configPluginId})");
|
||||
}
|
||||
|
||||
provider = new TranscriptionProvider
|
||||
{
|
||||
Num = 0, // will be set later by the PluginConfigurationObject
|
||||
@@ -115,6 +125,7 @@ public sealed record TranscriptionProvider(
|
||||
Hostname = hostname,
|
||||
Host = host,
|
||||
AllowUserProvidedAPIKey = allowUserProvidedApiKey,
|
||||
CustomIconDataUrl = customIconDataUrl,
|
||||
};
|
||||
|
||||
// Handle an encrypted API key if present. When the user manages their own key for this
|
||||
|
||||
Reference in new issue
Block a user