Merge branch 'main' into chunk-data

This commit is contained in:
PaulKoudelka committed 2026-05-26 17:19:32 +02:00
commit 00b6c98f3b
167 files changed
+8153 -3577

No files matched your search

@@ -4,7 +4,11 @@ public abstract class EmbeddingStore(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;
@@ -50,15 +54,6 @@ public abstract class EmbeddingStore(string name, string path)
{
this.logger = logService;
}
public abstract Task EnsureEmbeddingStoreExists(string collectionName, int vectorSize, CancellationToken token);
public abstract Task InsertEmbedding(string collectionName, IReadOnlyList<EmbeddingStoragePoint> points, CancellationToken token);
public abstract Task DeleteEmbeddingByFile(string collectionName, string filePath, CancellationToken token);
public abstract Task DeleteEmbeddingStore(string collectionName, CancellationToken token);
public abstract void Dispose();
}
@@ -0,0 +1,79 @@
using AIStudio.Tools.Rust;
namespace AIStudio.Tools.Databases;
public sealed partial class DatabaseClientProvider
{
private async Task<DatabaseClient> CreateQdrantClientAsync(CancellationToken cancellationToken)
{
var qdrantInfo = await this.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;
}
}
@@ -0,0 +1,180 @@
using AIStudio.Tools.Databases.Qdrant;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
namespace AIStudio.Tools.Databases;
public sealed class EmbeddingStoreProvider(RustService rustService, ILoggerFactory loggerFactory) : IDisposable
{
private readonly Dictionary<DatabaseRole, EmbeddingStore> clients = new();
private readonly Dictionary<DatabaseRole, SemaphoreSlim> locks = new();
private readonly Lock locksLock = new();
private readonly ILogger<EmbeddingStoreProvider> logger = loggerFactory.CreateLogger<EmbeddingStoreProvider>();
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,
}
@@ -0,0 +1,23 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Tools.Databases;
public sealed class NoEmbeddingStore(string name, string? unavailableReason, DatabaseClientStatus status = DatabaseClientStatus.UNAVAILABLE) : EmbeddingStore(name, string.Empty)
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoEmbeddingStore).Namespace, nameof(NoEmbeddingStore));
public override DatabaseClientStatus Status => status;
public override 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 override void Dispose()
{
}
}
@@ -1,39 +0,0 @@
using AIStudio.Tools.PluginSystem;
namespace AIStudio.Tools.Databases;
public sealed class NoEmbeddingStore(string name, string? unavailableReason) : EmbeddingStore(name, string.Empty)
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoEmbeddingStore).Namespace, nameof(NoEmbeddingStore));
public override bool IsAvailable => false;
public override 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 override Task EnsureEmbeddingStoreExists(string collectionName, int vectorSize, CancellationToken token) => throw this.BuildUnavailableException();
public override Task InsertEmbedding(string collectionName, IReadOnlyList<EmbeddingStoragePoint> points, CancellationToken token) => throw this.BuildUnavailableException();
public override Task DeleteEmbeddingByFile(string collectionName, string filePath, CancellationToken token) => Task.CompletedTask;
public override Task DeleteEmbeddingStore(string collectionName, CancellationToken token) => Task.CompletedTask;
public override void Dispose()
{
}
private InvalidOperationException BuildUnavailableException()
{
return new InvalidOperationException(string.IsNullOrWhiteSpace(unavailableReason)
? "The vector database is not available."
: unavailableReason);
}
}
@@ -28,6 +28,8 @@ public class QdrantClientImplementation : EmbeddingStore
this.ApiToken = apiToken ?? string.Empty;
this.GrpcClient = this.CreateQdrantClient();
}
public override string CacheKey => $"{this.Name}:{this.HttpPort}:{this.GrpcPort}:{this.Fingerprint}";
private const string IP_ADDRESS = "localhost";
@@ -49,6 +51,11 @@ public class QdrantClientImplementation : EmbeddingStore
return $"v{operation.Version}";
}
public async Task CheckAvailabilityAsync()
{
await this.GrpcClient.HealthAsync();
}
private async Task<string> GetCollectionsAmount()
{
var operation = await this.GrpcClient.ListCollectionsAsync();