added distinction between database and vectorstore

This commit is contained in:
PaulKoudelka 2026-05-28 12:36:21 +02:00
parent a15c47b56d
commit 66f108ef53
14 changed files with 382 additions and 212 deletions

View File

@ -6040,6 +6040,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1019424746"] = "Startup log file
-- Browse AI Studio's source code on GitHub — we welcome your contributions.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions."
-- Vector store version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1124039623"] = "Vector store version"
-- ID mismatch: the plugin ID differs from the enterprise configuration ID.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the plugin ID differs from the enterprise configuration ID."
@ -6052,9 +6055,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configur
-- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat."
-- Database version
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1420062548"] = "Database version"
-- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library."
@ -6202,6 +6202,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2924964415"] = "AI Studio runs w
-- Changelog
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog"
-- Vector store
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store"
-- Enterprise configuration ID:
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Enterprise configuration ID:"
@ -6277,9 +6280,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Check Pandoc Ins
-- Versions
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions"
-- Database
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database"
-- This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication."
@ -7003,20 +7003,29 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status"
-- Reason
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1093747001"] = "Reason"
-- Unavailable
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T3662391977"] = "Unavailable"
-- Status
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T6222351"] = "Status"
-- Storage size
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size"
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size"
-- HTTP port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP port"
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP port"
-- Reported version
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Reported version"
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Reported version"
-- gRPC port
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC port"
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC port"
-- Number of collections
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Number of collections"
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Number of collections"
-- The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::DATAMODEL::PROVIDERTYPEEXTENSIONS::T1555790630"] = "The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."

View File

@ -4,7 +4,7 @@ using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.AIJobs;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

View File

@ -88,7 +88,7 @@
<MetaAppCommitHash>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 8 ])</MetaAppCommitHash>
<MetaArchitecture>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 9 ])</MetaArchitecture>
<MetaPdfiumVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 10 ])</MetaPdfiumVersion>
<MetaQdrantVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 11 ])</MetaQdrantVersion>
<MetaVectorStoreVersion>$([System.String]::Copy( $(Metadata) ).Split( ';' )[ 11 ])</MetaVectorStoreVersion>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
@ -116,8 +116,8 @@
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataLibraries">
<_Parameter1>$(MetaPdfiumVersion)</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataDatabases">
<_Parameter1>$(MetaQdrantVersion)</_Parameter1>
<AssemblyAttribute Include="AIStudio.Tools.Metadata.MetaDataVectorStore">
<_Parameter1>$(MetaVectorStoreVersion)</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

View File

@ -21,11 +21,11 @@
<MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@this.VersionRust"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Storage">
<MudText Typo="Typo.body1">
@this.VersionDatabase
@this.VersionVectorStore
</MudText>
<MudCollapse Expanded="@this.showDatabaseDetails">
<MudCollapse Expanded="@this.showVectorStoreDetails">
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
@foreach (var item in this.databaseDisplayInfo)
@foreach (var item in this.vectorStoreDisplayInfo)
{
<div style="display: flex; align-items: center; gap: 8px;">
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
@ -35,11 +35,11 @@
}
</MudText>
</MudCollapse>
<MudButton StartIcon="@(this.showDatabaseDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
<MudButton StartIcon="@(this.showVectorStoreDetails ? Icons.Material.Filled.ExpandLess : Icons.Material.Filled.ExpandMore)"
Size="Size.Small"
Variant="Variant.Text"
OnClick="@this.ToggleDatabaseDetails">
@(this.showDatabaseDetails ? T("Hide Details") : T("Show Details"))
OnClick="@this.ToggleVectorStoreDetails">
@(this.showVectorStoreDetails ? T("Hide Details") : T("Show Details"))
</MudButton>
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Outlined.DocumentScanner" Text="@this.VersionPdfium"/>

View File

@ -35,7 +35,7 @@ public partial class Information : MSGComponentBase
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute<MetaDataArchitectureAttribute>()!;
private static readonly MetaDataLibrariesAttribute META_DATA_LIBRARIES = ASSEMBLY.GetCustomAttribute<MetaDataLibrariesAttribute>()!;
private static readonly MetaDataDatabasesAttribute META_DATA_DATABASES = ASSEMBLY.GetCustomAttribute<MetaDataDatabasesAttribute>()!;
private static readonly MetaDataVectorStoreAttribute META_DATA_VECTOR_STORE = ASSEMBLY.GetCustomAttribute<MetaDataVectorStoreAttribute>()!;
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Information).Namespace, nameof(Information));
@ -62,18 +62,18 @@ public partial class Information : MSGComponentBase
private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}";
private string VersionDatabase
private string VersionVectorStore
{
get
{
if (this.databaseClient is null)
return $"{T("Database")}: {T("checking availability")}";
if (this.vectorStore is null)
return $"{T("Vector store")}: {T("checking availability")}";
return this.databaseClient.Status switch
return this.vectorStore.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")}"
DatabaseClientStatus.AVAILABLE => $"{T("Vector store version")}: {this.vectorStore.Name} v{META_DATA_VECTOR_STORE.VectorStoreVersion}",
DatabaseClientStatus.STARTING => $"{T("Vector store")}: {this.vectorStore.Name} - {T("starting")}",
_ => $"{T("Vector store")}: {this.vectorStore.Name} - {T("not available")}"
};
}
}
@ -85,7 +85,7 @@ public partial class Information : MSGComponentBase
private bool showEnterpriseConfigDetails;
private bool showDatabaseDetails;
private bool showVectorStoreDetails;
private List<IAvailablePlugin> configPlugins = PluginFactory.AvailablePlugins
.Where(x => x.Type is PluginType.CONFIGURATION)
@ -95,14 +95,13 @@ public partial class Information : MSGComponentBase
private List<EnterpriseEnvironment> enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList();
private List<MandatoryInfoPanelData> mandatoryInfoPanels = [];
private sealed record DatabaseDisplayInfo(string Label, string Value);
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 sealed record VectorStoreDisplayInfo(string Label, string Value);
private readonly List<VectorStoreDisplayInfo> vectorStoreDisplayInfo = new();
private DatabaseClient? vectorStore;
private CancellationTokenSource? vectorStoreRefreshCancellationTokenSource;
private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive);
@ -148,9 +147,9 @@ public partial class Information : MSGComponentBase
this.osUserName = await this.RustService.ReadUserName();
this.logPaths = await this.RustService.GetLogPaths();
await this.RefreshDatabaseInfo(CancellationToken.None);
if (this.databaseClient?.Status is DatabaseClientStatus.STARTING)
this.StartShortDatabaseRefreshLoop();
await this.RefreshVectorStoreInfo(CancellationToken.None);
if (this.vectorStore?.Status is DatabaseClientStatus.STARTING)
this.StartShortVectorStoreRefreshLoop();
// Determine the Pandoc version may take some time, so we start it here
// without waiting for the result:
@ -249,22 +248,22 @@ public partial class Information : MSGComponentBase
this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails;
}
private void ToggleDatabaseDetails()
private void ToggleVectorStoreDetails()
{
this.showDatabaseDetails = !this.showDatabaseDetails;
this.showVectorStoreDetails = !this.showVectorStoreDetails;
}
private async Task RefreshDatabaseInfo(CancellationToken cancellationToken)
private async Task RefreshVectorStoreInfo(CancellationToken cancellationToken)
{
var refreshedClient = await this.DatabaseClientProvider.RefreshClientAsync(DatabaseRole.VECTOR_STORE, cancellationToken);
this.databaseClient = refreshedClient;
this.databaseDisplayInfo.Clear();
this.vectorStore = refreshedClient;
this.vectorStoreDisplayInfo.Clear();
try
{
await foreach (var (label, value) in refreshedClient.GetDisplayInfo().WithCancellation(cancellationToken))
{
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
this.vectorStoreDisplayInfo.Add(new VectorStoreDisplayInfo(label, value));
}
}
catch (OperationCanceledException)
@ -273,20 +272,20 @@ public partial class Information : MSGComponentBase
}
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.vectorStore = new NoDatabaseClient(refreshedClient.Name, e.Message, DatabaseClientStatus.STARTING);
await foreach (var (label, value) in this.vectorStore.GetDisplayInfo().WithCancellation(cancellationToken))
{
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
this.vectorStoreDisplayInfo.Add(new VectorStoreDisplayInfo(label, value));
}
}
}
private void StartShortDatabaseRefreshLoop()
private void StartShortVectorStoreRefreshLoop()
{
this.databaseRefreshCancellationTokenSource?.Cancel();
this.databaseRefreshCancellationTokenSource?.Dispose();
this.databaseRefreshCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = this.databaseRefreshCancellationTokenSource.Token;
this.vectorStoreRefreshCancellationTokenSource?.Cancel();
this.vectorStoreRefreshCancellationTokenSource?.Dispose();
this.vectorStoreRefreshCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = this.vectorStoreRefreshCancellationTokenSource.Token;
_ = Task.Run(async () =>
{
@ -298,11 +297,11 @@ public partial class Information : MSGComponentBase
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
await this.InvokeAsync(async () =>
{
await this.RefreshDatabaseInfo(cancellationToken);
await this.RefreshVectorStoreInfo(cancellationToken);
this.StateHasChanged();
});
if (this.databaseClient?.Status is not DatabaseClientStatus.STARTING)
if (this.vectorStore?.Status is not DatabaseClientStatus.STARTING)
return;
}
catch (OperationCanceledException)
@ -331,8 +330,8 @@ public partial class Information : MSGComponentBase
protected override void DisposeResources()
{
this.databaseRefreshCancellationTokenSource?.Cancel();
this.databaseRefreshCancellationTokenSource?.Dispose();
this.vectorStoreRefreshCancellationTokenSource?.Cancel();
this.vectorStoreRefreshCancellationTokenSource?.Dispose();
base.DisposeResources();
}

View File

@ -0,0 +1,80 @@
using AIStudio.Tools.Databases.VectorStore;
using AIStudio.Tools.Rust;
namespace AIStudio.Tools.Databases;
public sealed partial class DatabaseClientProvider
{
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;
}
}

View File

@ -1,10 +1,9 @@
using AIStudio.Tools.Databases.Qdrant;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using AIStudio.Tools.Databases.VectorStore;
namespace AIStudio.Tools.Databases;
public sealed class DatabaseClientProvider(RustService rustService, ILoggerFactory loggerFactory) : IDisposable
public sealed partial class DatabaseClientProvider(RustService rustService, ILoggerFactory loggerFactory) : IDisposable
{
private readonly Dictionary<DatabaseRole, DatabaseClient> clients = new();
private readonly Dictionary<DatabaseRole, SemaphoreSlim> locks = new();
@ -45,6 +44,18 @@ public sealed class DatabaseClientProvider(RustService rustService, ILoggerFacto
}
}
public async Task<IVectorStoreClient> GetVectorStoreAsync(CancellationToken cancellationToken = default)
{
var client = await this.GetClientAsync(DatabaseRole.VECTOR_STORE, cancellationToken);
if (client is IVectorStoreClient vectorStore)
return vectorStore;
return new NoVectorStoreClient(
client.Name,
"The configured database client does not support vector store operations.",
client.Status);
}
private DatabaseClient CacheIfAvailable(DatabaseRole databaseRole, DatabaseClient client)
{
if (!client.IsAvailable)
@ -84,79 +95,6 @@ public sealed class DatabaseClientProvider(RustService rustService, ILoggerFacto
_ => 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);

View File

@ -1,73 +0,0 @@
using Qdrant.Client;
using Qdrant.Client.Grpc;
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Tools.Databases.Qdrant;
public class QdrantClientImplementation : DatabaseClient
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(QdrantClientImplementation).Namespace, nameof(QdrantClientImplementation));
private int HttpPort { get; }
private int GrpcPort { get; }
private QdrantClient GrpcClient { get; }
private string Fingerprint { get; }
private string ApiToken { get; }
public QdrantClientImplementation(string name, string path, int httpPort, int grpcPort, string fingerprint, string apiToken): base(name, path)
{
this.HttpPort = httpPort;
this.GrpcPort = grpcPort;
this.Fingerprint = fingerprint;
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";
private QdrantClient CreateQdrantClient()
{
var address = "https://" + IP_ADDRESS + ":" + this.GrpcPort;
var channel = QdrantChannel.ForAddress(address, new ClientConfiguration
{
ApiKey = this.ApiToken,
CertificateThumbprint = this.Fingerprint
});
var grpcClient = new QdrantGrpcClient(channel);
return new QdrantClient(grpcClient);
}
private async Task<string> GetVersion()
{
var operation = await this.GrpcClient.HealthAsync();
return $"v{operation.Version}";
}
public async Task CheckAvailabilityAsync()
{
await this.GrpcClient.HealthAsync();
}
private async Task<string> GetCollectionsAmount()
{
var operation = await this.GrpcClient.ListCollectionsAsync();
return operation.Count.ToString();
}
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
{
yield return (TB("HTTP port"), this.HttpPort.ToString());
yield return (TB("gRPC port"), this.GrpcPort.ToString());
yield return (TB("Reported version"), await this.GetVersion());
yield return (TB("Storage size"), $"{this.GetStorageSize()}");
yield return (TB("Number of collections"), await this.GetCollectionsAmount());
}
public override void Dispose() => this.GrpcClient.Dispose();
}

View File

@ -0,0 +1,16 @@
namespace AIStudio.Tools.Databases;
public sealed record VectorStoragePoint(
string PointId,
IReadOnlyList<float> Vector,
string DataSourceId,
string DataSourceName,
string DataSourceType,
string FilePath,
string FileName,
string RelativePath,
int ChunkIndex,
string Text,
string Fingerprint,
DateTime LastWriteUtc,
DateTime EmbeddedAtUtc);

View File

@ -0,0 +1,20 @@
namespace AIStudio.Tools.Databases.VectorStore;
public interface IVectorStoreClient
{
string Name { get; }
DatabaseClientStatus Status { get; }
bool IsAvailable { get; }
IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo();
Task EnsureVectorStoreExists(string storeName, int vectorSize, CancellationToken token);
Task InsertEmbedding(string storeName, IReadOnlyList<VectorStoragePoint> points, CancellationToken token);
Task DeleteEmbeddingByFile(string storeName, string filePath, CancellationToken token);
Task DeleteVectorStore(string storeName, CancellationToken token);
}

View File

@ -0,0 +1,39 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Tools.Databases.VectorStore;
public sealed class NoVectorStoreClient(string name, string? unavailableReason, DatabaseClientStatus status = DatabaseClientStatus.UNAVAILABLE) : IVectorStoreClient
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoVectorStoreClient).Namespace, nameof(NoVectorStoreClient));
public string Name => name;
public DatabaseClientStatus Status => status;
public bool IsAvailable => false;
public async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
{
yield return (TB("Status"), TB("Unavailable"));
if (!string.IsNullOrWhiteSpace(unavailableReason))
yield return (TB("Reason"), unavailableReason);
await Task.CompletedTask;
}
public Task EnsureVectorStoreExists(string storeName, int vectorSize, CancellationToken token) =>
Task.FromException(this.CreateUnavailableException());
public Task InsertEmbedding(string storeName, IReadOnlyList<VectorStoragePoint> points, CancellationToken token) =>
Task.FromException(this.CreateUnavailableException());
public Task DeleteEmbeddingByFile(string storeName, string filePath, CancellationToken token) =>
Task.FromException(this.CreateUnavailableException());
public Task DeleteVectorStore(string storeName, CancellationToken token) =>
Task.FromException(this.CreateUnavailableException());
private InvalidOperationException CreateUnavailableException() =>
new(unavailableReason ?? "The vector store is not available.");
}

View File

@ -0,0 +1,142 @@
using Qdrant.Client;
using Qdrant.Client.Grpc;
using Grpc.Core;
using AIStudio.Tools.PluginSystem;
using static Qdrant.Client.Grpc.Conditions;
namespace AIStudio.Tools.Databases.VectorStore;
public class QdrantClientImplementation : DatabaseClient, IVectorStoreClient
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(QdrantClientImplementation).Namespace, nameof(QdrantClientImplementation));
private int HttpPort { get; }
private int GrpcPort { get; }
private QdrantClient GrpcClient { get; }
private string Fingerprint { get; }
private string ApiToken { get; }
public QdrantClientImplementation(string name, string path, int httpPort, int grpcPort, string fingerprint, string apiToken): base(name, path)
{
this.HttpPort = httpPort;
this.GrpcPort = grpcPort;
this.Fingerprint = fingerprint;
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";
private QdrantClient CreateQdrantClient()
{
var address = "https://" + IP_ADDRESS + ":" + this.GrpcPort;
var channel = QdrantChannel.ForAddress(address, new ClientConfiguration
{
ApiKey = this.ApiToken,
CertificateThumbprint = this.Fingerprint
});
var grpcClient = new QdrantGrpcClient(channel);
return new QdrantClient(grpcClient);
}
private async Task<string> GetVersion()
{
var operation = await this.GrpcClient.HealthAsync();
return $"v{operation.Version}";
}
public async Task CheckAvailabilityAsync()
{
await this.GrpcClient.HealthAsync();
}
private async Task<string> GetCollectionsAmount()
{
var operation = await this.GrpcClient.ListCollectionsAsync();
return operation.Count.ToString();
}
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
{
yield return (TB("HTTP port"), this.HttpPort.ToString());
yield return (TB("gRPC port"), this.GrpcPort.ToString());
yield return (TB("Reported version"), await this.GetVersion());
yield return (TB("Storage size"), $"{this.GetStorageSize()}");
yield return (TB("Number of collections"), await this.GetCollectionsAmount());
}
public async Task EnsureVectorStoreExists(string collectionName, int vectorSize, CancellationToken token)
{
var exists = await this.GrpcClient.CollectionExistsAsync(collectionName, token);
if (exists)
return;
await this.GrpcClient.CreateCollectionAsync(
collectionName,
new VectorParams
{
Size = (ulong)vectorSize,
Distance = Distance.Cosine,
},
cancellationToken: token);
}
public Task InsertEmbedding(string collectionName, IReadOnlyList<VectorStoragePoint> points, CancellationToken token)
{
var qdrantPoints = points.Select(point => new PointStruct
{
Id = Guid.Parse(point.PointId),
Vectors = point.Vector.ToArray(),
Payload =
{
["data_source_id"] = point.DataSourceId,
["data_source_name"] = point.DataSourceName,
["data_source_type"] = point.DataSourceType,
["file_path"] = point.FilePath,
["file_name"] = point.FileName,
["relative_path"] = point.RelativePath,
["chunk_index"] = (long)point.ChunkIndex,
["text"] = point.Text,
["fingerprint"] = point.Fingerprint,
["last_write_utc"] = point.LastWriteUtc.ToString("O"),
["embedded_at_utc"] = point.EmbeddedAtUtc.ToString("O"),
}
}).ToList();
return this.GrpcClient.UpsertAsync(collectionName, qdrantPoints, true, null, null, token);
}
public async Task DeleteEmbeddingByFile(string collectionName, string filePath, CancellationToken token)
{
try
{
await this.GrpcClient.DeleteAsync(collectionName, MatchKeyword("file_path", filePath), true, null, null, token);
}
catch (RpcException exception) when (exception.StatusCode is StatusCode.NotFound)
{
}
}
public async Task DeleteVectorStore(string collectionName, CancellationToken token)
{
var exists = await this.GrpcClient.CollectionExistsAsync(collectionName, token);
if (!exists)
return;
try
{
await this.GrpcClient.DeleteCollectionAsync(collectionName, cancellationToken: token);
}
catch (RpcException exception) when (exception.StatusCode is StatusCode.NotFound)
{
}
}
public override void Dispose() => this.GrpcClient.Dispose();
}

View File

@ -1,6 +0,0 @@
namespace AIStudio.Tools.Metadata;
public class MetaDataDatabasesAttribute(string databaseVersion) : Attribute
{
public string DatabaseVersion => databaseVersion;
}

View File

@ -0,0 +1,6 @@
namespace AIStudio.Tools.Metadata;
public class MetaDataVectorStoreAttribute(string vectorStoreVersion) : Attribute
{
public string VectorStoreVersion => vectorStoreVersion;
}