mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-05 03:49:40 +00:00
Improved Qdrant server startup & client initialization (#770)
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
Build and Release / Determine run mode (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
This commit is contained in:
1 parent
97e6003686
commit
cef1c99765
17 files changed
+443
-79
No files matched your search
@@ -6157,6 +6157,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Used .NET runtim
|
||||
-- Explanation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation"
|
||||
|
||||
-- checking availability
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "checking availability"
|
||||
|
||||
-- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software."
|
||||
|
||||
@@ -6265,6 +6268,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library
|
||||
-- Used .NET SDK
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK"
|
||||
|
||||
-- starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "starting"
|
||||
|
||||
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated."
|
||||
|
||||
@@ -6901,6 +6907,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "
|
||||
-- Reason
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason"
|
||||
|
||||
-- Starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Starting"
|
||||
|
||||
-- Unavailable
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable"
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ public partial class Information : MSGComponentBase
|
||||
private ISnackbar Snackbar { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private DatabaseClient DatabaseClient { get; init; } = null!;
|
||||
private DatabaseClientProvider DatabaseClientProvider { get; init; } = null!;
|
||||
|
||||
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
||||
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
||||
@@ -62,9 +62,21 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}";
|
||||
|
||||
private string VersionDatabase => this.DatabaseClient.IsAvailable
|
||||
? $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}"
|
||||
: $"{T("Database")}: {this.DatabaseClient.Name} - {T("not available")}";
|
||||
private string VersionDatabase
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.databaseClient is null)
|
||||
return $"{T("Database")}: {T("checking availability")}";
|
||||
|
||||
return this.databaseClient.Status switch
|
||||
{
|
||||
DatabaseClientStatus.AVAILABLE => $"{T("Database version")}: {this.databaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}",
|
||||
DatabaseClientStatus.STARTING => $"{T("Database")}: {this.databaseClient.Name} - {T("starting")}",
|
||||
_ => $"{T("Database")}: {this.databaseClient.Name} - {T("not available")}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private string versionPandoc = TB("Determine Pandoc version, please wait...");
|
||||
private PandocInstallation pandocInstallation;
|
||||
@@ -89,6 +101,8 @@ public partial class Information : MSGComponentBase
|
||||
private sealed record MandatoryInfoPanelData(string HeaderText, string PluginName, DataMandatoryInfo Info, DataMandatoryInfoAcceptance? Acceptance);
|
||||
|
||||
private readonly List<DatabaseDisplayInfo> databaseDisplayInfo = new();
|
||||
private DatabaseClient? databaseClient;
|
||||
private CancellationTokenSource? databaseRefreshCancellationTokenSource;
|
||||
|
||||
private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive);
|
||||
|
||||
@@ -134,10 +148,9 @@ public partial class Information : MSGComponentBase
|
||||
this.osUserName = await this.RustService.ReadUserName();
|
||||
this.logPaths = await this.RustService.GetLogPaths();
|
||||
|
||||
await foreach (var (label, value) in this.DatabaseClient.GetDisplayInfo())
|
||||
{
|
||||
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
|
||||
}
|
||||
await this.RefreshDatabaseInfo(CancellationToken.None);
|
||||
if (this.databaseClient?.Status is DatabaseClientStatus.STARTING)
|
||||
this.StartShortDatabaseRefreshLoop();
|
||||
|
||||
// Determine the Pandoc version may take some time, so we start it here
|
||||
// without waiting for the result:
|
||||
@@ -241,6 +254,69 @@ public partial class Information : MSGComponentBase
|
||||
this.showDatabaseDetails = !this.showDatabaseDetails;
|
||||
}
|
||||
|
||||
private async Task RefreshDatabaseInfo(CancellationToken cancellationToken)
|
||||
{
|
||||
var refreshedClient = await this.DatabaseClientProvider.RefreshClientAsync(DatabaseRole.VECTOR_STORE, cancellationToken);
|
||||
this.databaseClient = refreshedClient;
|
||||
this.databaseDisplayInfo.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var (label, value) in refreshedClient.GetDisplayInfo().WithCancellation(cancellationToken))
|
||||
{
|
||||
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.databaseClient = new NoDatabaseClient(refreshedClient.Name, e.Message, DatabaseClientStatus.STARTING);
|
||||
await foreach (var (label, value) in this.databaseClient.GetDisplayInfo().WithCancellation(cancellationToken))
|
||||
{
|
||||
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartShortDatabaseRefreshLoop()
|
||||
{
|
||||
this.databaseRefreshCancellationTokenSource?.Cancel();
|
||||
this.databaseRefreshCancellationTokenSource?.Dispose();
|
||||
this.databaseRefreshCancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = this.databaseRefreshCancellationTokenSource.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
const int MAX_TRIES = 12;
|
||||
for (var attempt = 0; attempt < MAX_TRIES; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
await this.InvokeAsync(async () =>
|
||||
{
|
||||
await this.RefreshDatabaseInfo(cancellationToken);
|
||||
this.StateHasChanged();
|
||||
});
|
||||
|
||||
if (this.databaseClient?.Status is not DatabaseClientStatus.STARTING)
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId)
|
||||
{
|
||||
return this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId == configurationId)
|
||||
@@ -253,6 +329,13 @@ public partial class Information : MSGComponentBase
|
||||
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
|
||||
}
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
this.databaseRefreshCancellationTokenSource?.Cancel();
|
||||
this.databaseRefreshCancellationTokenSource?.Dispose();
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
private async Task CopyStartupLogPath()
|
||||
{
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
|
||||
|
||||
+9
@@ -6159,6 +6159,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Verwendete .NET-
|
||||
-- Explanation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Erklärung"
|
||||
|
||||
-- checking availability
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "Verfügbarkeit wird geprüft"
|
||||
|
||||
-- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend kann nicht als Desktop-App gestartet werden. Deshalb verwende ich ein zweites Backend in Rust, das ich „Runtime“ nenne. Mit Rust als Runtime kann Tauri genutzt werden, um eine typische Desktop-App zu realisieren. Dank Rust kann diese App für Windows-, macOS- und Linux-Desktops angeboten werden. Rust ist eine großartige Sprache für die Entwicklung sicherer und leistungsstarker Software."
|
||||
|
||||
@@ -6267,6 +6270,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "Dies ist eine Bib
|
||||
-- Used .NET SDK
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Verwendetes .NET SDK"
|
||||
|
||||
-- starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "wird gestartet"
|
||||
|
||||
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "Diese Bibliothek wird verwendet, um Sidecar-Prozesse zu verwalten und sicherzustellen, dass veraltete oder Zombie-Sidecars erkannt und beendet werden."
|
||||
|
||||
@@ -6903,6 +6909,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "
|
||||
-- Reason
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Grund"
|
||||
|
||||
-- Starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Wird gestartet"
|
||||
|
||||
-- Unavailable
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Nicht verfügbar"
|
||||
|
||||
|
||||
+9
@@ -6159,6 +6159,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Used .NET runtim
|
||||
-- Explanation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation"
|
||||
|
||||
-- checking availability
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "checking availability"
|
||||
|
||||
-- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software."
|
||||
|
||||
@@ -6267,6 +6270,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library
|
||||
-- Used .NET SDK
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK"
|
||||
|
||||
-- starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "starting"
|
||||
|
||||
-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated."
|
||||
|
||||
@@ -6903,6 +6909,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "
|
||||
-- Reason
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason"
|
||||
|
||||
-- Starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Starting"
|
||||
|
||||
-- Unavailable
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable"
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ using AIStudio.Agents;
|
||||
using AIStudio.Agents.AssistantAudit;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Databases;
|
||||
using AIStudio.Tools.Databases.Qdrant;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
using AIStudio.Tools.Services;
|
||||
@@ -28,7 +27,7 @@ internal sealed class Program
|
||||
public static string API_TOKEN = null!;
|
||||
public static IServiceProvider SERVICE_PROVIDER = null!;
|
||||
public static ILoggerFactory LOGGER_FACTORY = null!;
|
||||
public static DatabaseClient DATABASE_CLIENT = null!;
|
||||
public static DatabaseClientProvider DATABASE_CLIENT_PROVIDER = null!;
|
||||
|
||||
public static async Task Main()
|
||||
{
|
||||
@@ -87,48 +86,6 @@ internal sealed class Program
|
||||
return;
|
||||
}
|
||||
|
||||
var qdrantInfo = await rust.GetQdrantInfo();
|
||||
DatabaseClient databaseClient;
|
||||
if (!qdrantInfo.IsAvailable)
|
||||
{
|
||||
Console.WriteLine($"Warning: Qdrant is not available. Starting without vector database. Reason: '{qdrantInfo.UnavailableReason ?? "unknown"}'.");
|
||||
databaseClient = new NoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (qdrantInfo.Path == string.Empty)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to get the Qdrant path from Rust.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (qdrantInfo.PortHttp == 0)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (qdrantInfo.PortGrpc == 0)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (qdrantInfo.Fingerprint == string.Empty)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (qdrantInfo.ApiToken == string.Empty)
|
||||
{
|
||||
Console.WriteLine("Error: Failed to get the Qdrant API token from Rust.");
|
||||
return;
|
||||
}
|
||||
|
||||
databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken);
|
||||
}
|
||||
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
|
||||
{
|
||||
@@ -183,7 +140,7 @@ internal sealed class Program
|
||||
builder.Services.AddHostedService<UpdateService>();
|
||||
builder.Services.AddHostedService<TemporaryChatService>();
|
||||
builder.Services.AddHostedService<EnterpriseEnvironmentService>();
|
||||
builder.Services.AddSingleton(databaseClient);
|
||||
builder.Services.AddSingleton<DatabaseClientProvider>();
|
||||
builder.Services.AddHostedService<GlobalShortcutService>();
|
||||
builder.Services.AddHostedService<RustAvailabilityMonitorService>();
|
||||
|
||||
@@ -242,10 +199,7 @@ internal sealed class Program
|
||||
|
||||
RUST_SERVICE = rust;
|
||||
ENCRYPTION = encryption;
|
||||
|
||||
var databaseLogger = app.Services.GetRequiredService<ILogger<DatabaseClient>>();
|
||||
databaseClient.SetLogger(databaseLogger);
|
||||
DATABASE_CLIENT = databaseClient;
|
||||
DATABASE_CLIENT_PROVIDER = app.Services.GetRequiredService<DatabaseClientProvider>();
|
||||
|
||||
programLogger.LogInformation("Initialize internal file system.");
|
||||
app.Use(Redirect.HandlerContentAsync);
|
||||
@@ -283,7 +237,7 @@ internal sealed class Program
|
||||
await serverTask;
|
||||
|
||||
RUST_SERVICE.Dispose();
|
||||
DATABASE_CLIENT.Dispose();
|
||||
DATABASE_CLIENT_PROVIDER.Dispose();
|
||||
PluginFactory.Dispose();
|
||||
programLogger.LogInformation("The AI Studio server was stopped.");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ public abstract class DatabaseClient(string name, string path)
|
||||
{
|
||||
public string Name => name;
|
||||
|
||||
public virtual bool IsAvailable => true;
|
||||
public virtual string CacheKey => name;
|
||||
|
||||
public virtual DatabaseClientStatus Status => DatabaseClientStatus.AVAILABLE;
|
||||
|
||||
public bool IsAvailable => this.Status is DatabaseClientStatus.AVAILABLE;
|
||||
|
||||
private string Path => path;
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
using AIStudio.Tools.Databases.Qdrant;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tools.Databases;
|
||||
|
||||
public sealed class DatabaseClientProvider(RustService rustService, ILoggerFactory loggerFactory) : IDisposable
|
||||
{
|
||||
private readonly Dictionary<DatabaseRole, DatabaseClient> clients = new();
|
||||
private readonly Dictionary<DatabaseRole, SemaphoreSlim> locks = new();
|
||||
private readonly Lock locksLock = new();
|
||||
private readonly ILogger<DatabaseClientProvider> logger = loggerFactory.CreateLogger<DatabaseClientProvider>();
|
||||
private readonly ILogger<DatabaseClient> databaseClientLogger = loggerFactory.CreateLogger<DatabaseClient>();
|
||||
|
||||
public async Task<DatabaseClient> GetClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var databaseLock = this.GetLock(databaseRole);
|
||||
await databaseLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (this.clients.TryGetValue(databaseRole, out var cachedClient) && cachedClient.IsAvailable)
|
||||
return cachedClient;
|
||||
|
||||
var client = await this.CreateClientAsync(databaseRole, cancellationToken);
|
||||
return this.CacheIfAvailable(databaseRole, client);
|
||||
}
|
||||
finally
|
||||
{
|
||||
databaseLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DatabaseClient> RefreshClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var databaseLock = this.GetLock(databaseRole);
|
||||
await databaseLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var client = await this.CreateClientAsync(databaseRole, cancellationToken);
|
||||
return this.CacheIfAvailable(databaseRole, client);
|
||||
}
|
||||
finally
|
||||
{
|
||||
databaseLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseClient CacheIfAvailable(DatabaseRole databaseRole, DatabaseClient client)
|
||||
{
|
||||
if (!client.IsAvailable)
|
||||
return client;
|
||||
|
||||
if (this.clients.TryGetValue(databaseRole, out var cachedClient))
|
||||
{
|
||||
if (IsSameClient(cachedClient, client))
|
||||
{
|
||||
client.Dispose();
|
||||
return cachedClient;
|
||||
}
|
||||
|
||||
cachedClient.Dispose();
|
||||
}
|
||||
|
||||
this.clients[databaseRole] = client;
|
||||
return client;
|
||||
}
|
||||
|
||||
private SemaphoreSlim GetLock(DatabaseRole databaseRole)
|
||||
{
|
||||
lock (this.locksLock)
|
||||
{
|
||||
if (this.locks.TryGetValue(databaseRole, out var databaseLock))
|
||||
return databaseLock;
|
||||
|
||||
databaseLock = new SemaphoreSlim(1, 1);
|
||||
this.locks[databaseRole] = databaseLock;
|
||||
return databaseLock;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<DatabaseClient> CreateClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken) => databaseRole switch
|
||||
{
|
||||
DatabaseRole.VECTOR_STORE => await this.CreateQdrantClientAsync(cancellationToken),
|
||||
_ => new NoDatabaseClient(databaseRole.ToString(), "The requested database role is not supported.")
|
||||
};
|
||||
|
||||
private async Task<DatabaseClient> CreateQdrantClientAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var qdrantInfo = await rustService.GetQdrantInfo(cancellationToken);
|
||||
if (qdrantInfo.Status is QdrantStatus.STARTING)
|
||||
{
|
||||
return this.CreateNoDatabaseClient(
|
||||
"Qdrant",
|
||||
"Qdrant is starting. Details will appear shortly.",
|
||||
DatabaseClientStatus.STARTING);
|
||||
}
|
||||
|
||||
if (!qdrantInfo.IsAvailable || qdrantInfo.Status is QdrantStatus.UNAVAILABLE)
|
||||
{
|
||||
var reason = qdrantInfo.UnavailableReason ?? "unknown";
|
||||
this.logger.LogWarning("Qdrant is not available. Starting without vector database. Reason: '{Reason}'.", reason);
|
||||
return this.CreateNoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason, DatabaseClientStatus.UNAVAILABLE);
|
||||
}
|
||||
|
||||
if (!HasValidQdrantConnectionInfo(qdrantInfo, out var invalidReason))
|
||||
return this.CreateNoDatabaseClient("Qdrant", invalidReason, DatabaseClientStatus.UNAVAILABLE);
|
||||
|
||||
var client = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken);
|
||||
client.SetLogger(this.databaseClientLogger);
|
||||
|
||||
try
|
||||
{
|
||||
await client.CheckAvailabilityAsync();
|
||||
return client;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
client.Dispose();
|
||||
this.logger.LogWarning(e, "Qdrant reported as available by Rust, but the health check failed.");
|
||||
return this.CreateNoDatabaseClient("Qdrant", e.Message, DatabaseClientStatus.STARTING);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasValidQdrantConnectionInfo(QdrantInfo qdrantInfo, out string invalidReason)
|
||||
{
|
||||
if (qdrantInfo.Path == string.Empty)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant path from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.PortHttp == 0)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant HTTP port from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.PortGrpc == 0)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant gRPC port from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.Fingerprint == string.Empty)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant fingerprint from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.ApiToken == string.Empty)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant API token from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
invalidReason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private NoDatabaseClient CreateNoDatabaseClient(string name, string? unavailableReason, DatabaseClientStatus status)
|
||||
{
|
||||
var client = new NoDatabaseClient(name, unavailableReason, status);
|
||||
client.SetLogger(this.databaseClientLogger);
|
||||
return client;
|
||||
}
|
||||
|
||||
private static bool IsSameClient(DatabaseClient left, DatabaseClient right) =>
|
||||
left.IsAvailable
|
||||
&& right.IsAvailable
|
||||
&& left.CacheKey == right.CacheKey;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var client in this.clients.Values)
|
||||
client.Dispose();
|
||||
|
||||
foreach (var databaseLock in this.locks.Values)
|
||||
databaseLock.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Tools.Databases;
|
||||
|
||||
public enum DatabaseClientStatus
|
||||
{
|
||||
STARTING,
|
||||
AVAILABLE,
|
||||
UNAVAILABLE,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace AIStudio.Tools.Databases;
|
||||
|
||||
public enum DatabaseRole
|
||||
{
|
||||
VECTOR_STORE,
|
||||
}
|
||||
@@ -2,15 +2,19 @@ using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Tools.Databases;
|
||||
|
||||
public sealed class NoDatabaseClient(string name, string? unavailableReason) : DatabaseClient(name, string.Empty)
|
||||
public sealed class NoDatabaseClient(string name, string? unavailableReason, DatabaseClientStatus status = DatabaseClientStatus.UNAVAILABLE) : DatabaseClient(name, string.Empty)
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoDatabaseClient).Namespace, nameof(NoDatabaseClient));
|
||||
|
||||
public override bool IsAvailable => false;
|
||||
public override DatabaseClientStatus Status => status;
|
||||
|
||||
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
|
||||
{
|
||||
yield return (TB("Status"), TB("Unavailable"));
|
||||
yield return (TB("Status"), status switch
|
||||
{
|
||||
DatabaseClientStatus.STARTING => TB("Starting"),
|
||||
_ => TB("Unavailable")
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(unavailableReason))
|
||||
yield return (TB("Reason"), unavailableReason);
|
||||
|
||||
@@ -26,6 +26,8 @@ public class QdrantClientImplementation : DatabaseClient
|
||||
this.ApiToken = apiToken;
|
||||
this.GrpcClient = this.CreateQdrantClient();
|
||||
}
|
||||
|
||||
public override string CacheKey => $"{this.Name}:{this.HttpPort}:{this.GrpcPort}:{this.Fingerprint}";
|
||||
|
||||
private const string IP_ADDRESS = "localhost";
|
||||
|
||||
@@ -47,6 +49,11 @@ public class QdrantClientImplementation : DatabaseClient
|
||||
return $"v{operation.Version}";
|
||||
}
|
||||
|
||||
public async Task CheckAvailabilityAsync()
|
||||
{
|
||||
await this.GrpcClient.HealthAsync();
|
||||
}
|
||||
|
||||
private async Task<string> GetCollectionsAmount()
|
||||
{
|
||||
var operation = await this.GrpcClient.ListCollectionsAsync();
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
/// </summary>
|
||||
public readonly record struct QdrantInfo
|
||||
{
|
||||
public QdrantStatus Status { get; init; }
|
||||
|
||||
public bool IsAvailable { get; init; }
|
||||
|
||||
public string? UnavailableReason { get; init; }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Tools.Rust;
|
||||
|
||||
public enum QdrantStatus
|
||||
{
|
||||
STARTING,
|
||||
AVAILABLE,
|
||||
UNAVAILABLE,
|
||||
}
|
||||
@@ -4,13 +4,27 @@ namespace AIStudio.Tools.Services;
|
||||
|
||||
public sealed partial class RustService
|
||||
{
|
||||
public async Task<QdrantInfo> GetQdrantInfo()
|
||||
public async Task<QdrantInfo> GetQdrantInfo(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
|
||||
var response = await this.http.GetFromJsonAsync<QdrantInfo>("/system/qdrant/info", this.jsonRustSerializerOptions, cts.Token);
|
||||
return response;
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(45));
|
||||
|
||||
return await this.http.GetFromJsonAsync<QdrantInfo>("/system/qdrant/info", this.jsonRustSerializerOptions, cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if(this.logger is not null)
|
||||
this.logger.LogWarning("Fetching Qdrant info from Rust service was cancelled by caller.");
|
||||
else
|
||||
Console.WriteLine("Fetching Qdrant info from Rust service was cancelled by caller.");
|
||||
|
||||
return new QdrantInfo
|
||||
{
|
||||
Status = QdrantStatus.UNAVAILABLE,
|
||||
UnavailableReason = "Operation cancelled by caller."
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -19,7 +33,11 @@ public sealed partial class RustService
|
||||
else
|
||||
Console.WriteLine($"Error while fetching Qdrant info from Rust service: '{e}'.");
|
||||
|
||||
return default;
|
||||
return new QdrantInfo
|
||||
{
|
||||
Status = QdrantStatus.UNAVAILABLE,
|
||||
UnavailableReason = e.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
- Added the username to the information page to make organization support easier when users share their screen.
|
||||
- Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base.
|
||||
- Improved the Pandoc management and detection process to make it more reliable.
|
||||
- Improved the Qdrant startup and vector database initialization, so AI Studio can start more reliably while the local vector database is still starting.
|
||||
- Fixed the Pandoc installation, which could fail and prevent AI Studio from installing its local Pandoc dependency.
|
||||
- Fixed an issue where the spellchecking setting was not applied to all text fields in the slide builder assistant.
|
||||
- Fixed missing translations for file type names in file selection dialogs.
|
||||
|
||||
Reference in new issue
Block a user