Added pipeline to retrieve information for .NET runtime to create Qdrant client

This commit is contained in:
PaulKoudelka 2025-12-11 14:40:08 +01:00
parent dec2c27997
commit a79a2996fe
19 changed files with 232 additions and 60 deletions

View File

@ -174,7 +174,7 @@ jobs:
pdfium_version=$(echo $pdfium_version | cut -d'.' -f3)
# Next line is the Qdrant version:
qdrant_version=$(sed -n '12p' metadata.txt)
qdrant_version="v$(sed -n '12p' metadata.txt)"
# Write the metadata to the environment:
echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
@ -247,7 +247,7 @@ jobs:
$pdfium_version = $pdfium_version.Split('.')[2]
# Next line is the necessary Qdrant version:
$qdrant_version = $metadata[12]
$qdrant_version = "v$metadata[12]"
# Write the metadata to the environment:
Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV
@ -441,7 +441,7 @@ jobs:
;;
esac
QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSON}/qdrant-{QDRANT_FILE}"
QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/v${QDRANT_VERSION}/qdrant-{QDRANT_FILE}"
echo "Download Qdrant $QDRANT_URL ..."
TMP=$(mktemp -d)
@ -485,7 +485,7 @@ jobs:
}
}
QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSON}/qdrant-{QDRANT_FILE}"
QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/v${QDRANT_VERSION}/qdrant-{QDRANT_FILE}"
Write-Host "Download $QDRANT_URL ..."
# Create a unique temporary directory (not just a file)

View File

@ -32,7 +32,7 @@ Since November 2024: Work on RAG (integration of your data and files) has begun.
- [x] ~~App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393), [PR #487](https://github.com/MindWorkAI/AI-Studio/pull/487))~~
- [ ] App: Implement external embedding providers
- [ ] App: Implement the process to vectorize one local file using embeddings
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
- [ ] Runtime: Integration of the vector database [Qdrant](https://github.com/qdrant/qdrant)
- [ ] App: Implement the continuous process of vectorizing data
- [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~
- [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~

View File

@ -88,11 +88,11 @@ public static class Qdrant
private static Database GetDatabasePath(RID rid) => rid switch
{
RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"),
RID.LINUX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"),
RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"),
RID.OSX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"),
RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-gnu"),
RID.OSX_X64 => new("qdrant", "qdrant-x86_64-unknown-linux-gnu"),
RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-gnu"),
RID.LINUX_X64 => new("qdrant", "qdrant-x86_64-unknown-linux-gnu"),
RID.WIN_X64 => new("qdrant.exe", "qdrant-x86_64-pc-windows-msvc.exe"),
@ -101,7 +101,7 @@ public static class Qdrant
private static string GetQdrantDownloadUrl(RID rid, string version)
{
var baseUrl = $"https://github.com/qdrant/qdrant/releases/download/{version}/qdrant-";
var baseUrl = $"https://github.com/qdrant/qdrant/releases/download/v{version}/qdrant-";
return rid switch
{
RID.LINUX_ARM64 => $"{baseUrl}aarch64-unknown-linux-musl.tar.gz",

View File

@ -4462,6 +4462,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1282228996"] = "AI Studio runs with an
-- 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::ABOUT::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::ABOUT::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::ABOUT::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library."
@ -4501,6 +4504,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1924365263"] = "This library is used t
-- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust."
-- Copies the following to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2029659664"] = "Copies the following to the clipboard"
-- Copies the server URL to the clipboard
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2037899437"] = "Copies the server URL to the clipboard"
@ -4567,9 +4573,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2777988282"] = "Code in the Rust langu
-- Show Details
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T27924674"] = "Show Details"
-- Used Qdrant version
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2799791022"] = "Used Qdrant version"
-- View our project roadmap and help shape AI Studio's future development.
UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2829971158"] = "View our project roadmap and help shape AI Studio's future development."

View File

@ -19,7 +19,29 @@
<MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@this.VersionDotnetSdk"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@this.VersionDotnetRuntime"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Build" Text="@this.VersionRust"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Storage" Text="@this.VersionQdrant"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Storage">
<MudText Typo="Typo.body1">
@this.VersionDatabase
</MudText>
<MudCollapse Expanded="@showDatabaseDetails">
<MudText Typo="Typo.body1" Class="mt-2 mb-2">
@foreach (var (Label, Value) in DatabaseClient.GetDisplayInfo())
{
<div style="display: flex; align-items: center; gap: 8px;">
<MudIcon Icon="@Icons.Material.Filled.ArrowRightAlt"/>
<span>@Label: @Value</span>
<MudCopyClipboardButton TooltipMessage="@(T("Copies the following to the clipboard")+": "+Value)" StringContent=@Value/>
</div>
}
</MudText>
</MudCollapse>
<MudButton StartIcon="@(this.showDatabaseDetails ? 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"))
</MudButton>
</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Outlined.DocumentScanner" Text="@this.VersionPdfium"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Article" Text="@this.versionPandoc"/>
<MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/>

View File

@ -2,6 +2,7 @@ using System.Reflection;
using AIStudio.Components;
using AIStudio.Dialogs;
using AIStudio.Tools.Databases;
using AIStudio.Tools.Metadata;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
@ -26,6 +27,9 @@ public partial class About : MSGComponentBase
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private DatabaseClient DatabaseClient { get; init; } = null!;
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute<MetaDataArchitectureAttribute>()!;
@ -54,7 +58,7 @@ public partial class About : MSGComponentBase
private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}";
private string VersionQdrant => $"{T("Used Qdrant version")}: {META_DATA_DATABASES.QdrantVersion}";
private string VersionDatabase => $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}";
private string versionPandoc = TB("Determine Pandoc version, please wait...");
private PandocInstallation pandocInstallation;
@ -63,6 +67,8 @@ public partial class About : MSGComponentBase
private bool showEnterpriseConfigDetails;
private bool showDatabaseDetails = false;
private IPluginMetadata? configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION);
/// <summary>
@ -173,6 +179,11 @@ public partial class About : MSGComponentBase
{
this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails;
}
private void ToggleDatabaseDetails()
{
this.showDatabaseDetails = !this.showDatabaseDetails;
}
private async Task CopyStartupLogPath()
{

View File

@ -1,6 +1,9 @@
using AIStudio.Agents;
using AIStudio.Settings;
using AIStudio.Tools.Databases;
using AIStudio.Tools.Databases.Qdrant;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Server.Kestrel.Core;
@ -82,6 +85,24 @@ internal sealed class Program
return;
}
var qdrantInfo = await rust.GetQdrantInfo();
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;
}
var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(kestrelServerOptions =>
@ -133,6 +154,7 @@ internal sealed class Program
builder.Services.AddHostedService<UpdateService>();
builder.Services.AddHostedService<TemporaryChatService>();
builder.Services.AddHostedService<EnterpriseEnvironmentService>();
builder.Services.AddSingleton<DatabaseClient>(new QdrantClient("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc));
// ReSharper disable AccessToDisposedClosure
builder.Services.AddHostedService<RustService>(_ => rust);
@ -215,6 +237,7 @@ internal sealed class Program
await rust.AppIsReady();
programLogger.LogInformation("The AI Studio server is ready.");
TaskScheduler.UnobservedTaskException += (sender, taskArgs) =>
{
programLogger.LogError(taskArgs.Exception, $"Unobserved task exception by sender '{sender ?? "n/a"}'.");

View File

@ -77,7 +77,6 @@ public record Profile(
public static bool TryParseProfileTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject template)
{
LOGGER.LogInformation($"\n Profile table parsing {idx}.\n");
template = NO_PROFILE;
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
{

View File

@ -0,0 +1,71 @@
namespace AIStudio.Tools.Databases;
public abstract class DatabaseClient
{
public string Name { get; }
private string Path { get; }
public DatabaseClient(string name, string path)
{
this.Name = name;
this.Path = path;
}
public abstract IEnumerable<(string Label, string Value)> GetDisplayInfo();
public string GetStorageSize()
{
if (string.IsNullOrEmpty(this.Path))
{
Console.WriteLine($"Error: Database path '{this.Path}' cannot be null or empty.");
return "0 B";
}
if (!Directory.Exists(this.Path))
{
Console.WriteLine($"Error: Database path '{this.Path}' does not exist.");
return "0 B";
}
long size = 0;
var stack = new Stack<string>();
stack.Push(this.Path);
while (stack.Count > 0)
{
string directory = stack.Pop();
try
{
var files = Directory.GetFiles(directory);
size += files.Sum(file => new FileInfo(file).Length);
var subDirectories = Directory.GetDirectories(directory);
foreach (var subDirectory in subDirectories)
{
stack.Push(subDirectory);
}
}
catch (UnauthorizedAccessException)
{
Console.WriteLine($"No access to {directory}");
}
catch (Exception ex)
{
Console.WriteLine($"An error encountered while processing {directory}: ");
Console.WriteLine($"{ ex.Message}");
}
}
return FormatBytes(size);
}
public static string FormatBytes(long size)
{
string[] suffixes = { "B", "KB", "MB", "GB", "TB", "PB" };
int suffixIndex = 0;
while (size >= 1024 && suffixIndex < suffixes.Length - 1)
{
size /= 1024;
suffixIndex++;
}
return $"{size:0##} {suffixes[suffixIndex]}";
}
}

View File

@ -0,0 +1,15 @@
namespace AIStudio.Tools.Databases.Qdrant;
public class QdrantClient(string name, string path, int httpPort, int grpcPort) : DatabaseClient(name, path)
{
private int HttpPort { get; } = httpPort;
private int GrpcPort { get; } = grpcPort;
private string IpAddress { get; } = "127.0.0.1";
public override IEnumerable<(string Label, string Value)> GetDisplayInfo()
{
yield return ("HTTP Port", this.HttpPort.ToString());
yield return ("gRPC Port", this.GrpcPort.ToString());
yield return ("Storage Size", $"{base.GetStorageSize()}");
}
}

View File

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

View File

@ -0,0 +1,13 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// The response of the Qdrant information request.
/// </summary>
/// <param name="portHTTP">The port number for HTTP communication with Qdrant.</param>
/// <param name="portGRPC">The port number for gRPC communication with Qdrant</param>
public record struct QdrantInfo
{
public string Path { get; init; }
public int PortHttp { get; init; }
public int PortGrpc { get; init; }
}

View File

@ -0,0 +1,26 @@
using AIStudio.Tools.Rust;
namespace AIStudio.Tools.Services;
public sealed partial class RustService
{
public async Task<QdrantInfo> GetQdrantInfo()
{
try
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
var response = await this.http.GetFromJsonAsync<QdrantInfo>("/system/qdrant/port", this.jsonRustSerializerOptions, cts.Token);
return response;
}
catch (Exception e)
{
Console.WriteLine(e);
return new QdrantInfo
{
Path = string.Empty,
PortHttp = 0,
PortGrpc = 0,
};
}
}
}

View File

@ -1 +1,2 @@
# v0.9.55, build 230 (2025-12-xx xx:xx UTC)
Added functionality to download Qdrant and execute it as a background sidecar.

View File

@ -9,4 +9,4 @@
009bb33d839, release
osx-arm64
137.0.7215.0
v1.16.1
1.16.1

View File

@ -235,15 +235,15 @@ service:
max_workers: 0
# Host to bind the service on
host: 0.0.0.0
host: 127.0.0.1
# HTTP(S) port to bind the service on
http_port: 6373
# http_port: 6333
# gRPC port to bind the service on.
# If `null` - gRPC is disabled. Default: null
# Comment to disable gRPC:
grpc_port: 6344
# grpc_port: 6334
# Enable CORS headers in REST API.
# If enabled, browsers would be allowed to query REST endpoints regardless of query origin.
@ -326,7 +326,7 @@ cluster:
# Set to true to prevent service from sending usage statistics to the developers.
# Read more: https://qdrant.tech/documentation/guides/telemetry
telemetry_disabled: false
telemetry_disabled: true
# TLS configuration.
# Required if either service.enable_tls or cluster.p2p.enable_tls is true.

View File

@ -38,7 +38,7 @@ async fn main() {
info!(".. MudBlazor: v{mud_blazor_version}", mud_blazor_version = metadata.mud_blazor_version);
info!(".. Tauri: v{tauri_version}", tauri_version = metadata.tauri_version);
info!(".. PDFium: v{pdfium_version}", pdfium_version = metadata.pdfium_version);
info!(".. Qdrant: {qdrant_version}", qdrant_version = metadata.qdrant_version);
info!(".. Qdrant: v{qdrant_version}", qdrant_version = metadata.qdrant_version);
if is_dev() {
warn!("Running in development mode.");

View File

@ -4,8 +4,9 @@ use std::sync::{Arc, Mutex};
use log::{debug, error, info, warn};
use once_cell::sync::Lazy;
use rocket::get;
use rocket::serde::json::Json;
use rocket::serde::Serialize;
use tauri::api::process::{Command, CommandChild, CommandEvent};
use tauri::Url;
use crate::api_token::{APIToken};
use crate::environment::DATA_DIRECTORY;
@ -14,16 +15,28 @@ use crate::environment::DATA_DIRECTORY;
static QDRANT_SERVER: Lazy<Arc<Mutex<Option<CommandChild>>>> = Lazy::new(|| Arc::new(Mutex::new(None)));
// Qdrant server port (default is 6333 for HTTP and 6334 for gRPC)
static QDRANT_SERVER_PORT: Lazy<u16> = Lazy::new(|| {
static QDRANT_SERVER_PORT_HTTP: Lazy<u16> = Lazy::new(|| {
crate::network::get_available_port().unwrap_or(6333)
});
});
static QDRANT_INITIALIZED: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
static QDRANT_SERVER_PORT_GRPC: Lazy<u16> = Lazy::new(|| {
crate::network::get_available_port().unwrap_or(6334)
});
#[derive(Serialize)]
pub struct ProvideQdrantInfo {
path: String,
port_http: u16,
port_grpc: u16,
}
#[get("/system/qdrant/port")]
pub fn qdrant_port(_token: APIToken) -> String {
let qdrant_port = *QDRANT_SERVER_PORT;
format!("{qdrant_port}")
pub fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> {
return Json(ProvideQdrantInfo {
path: Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").to_str().unwrap().to_string(),
port_http: *QDRANT_SERVER_PORT_HTTP,
port_grpc: *QDRANT_SERVER_PORT_GRPC,
});
}
/// Starts the Qdrant server in a separate process.
@ -35,16 +48,14 @@ pub fn start_qdrant_server() {
let snapshot_path = Path::new(base_path).join("databases").join("qdrant").join("snapshots").to_str().unwrap().to_string();
let init_path = Path::new(base_path).join("databases").join("qdrant").join(".qdrant-initalized").to_str().unwrap().to_string();
println!("{}", storage_path);
println!("{}", snapshot_path);
println!("{}", init_path);
let qdrant_server_environment = HashMap::from_iter([
(String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT.to_string()),
(String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()),
(String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()),
(String::from("QDRANT_INIT_FILE_PATH"), init_path),
(String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path),
(String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path),
]);
let server_spawn_clone = QDRANT_SERVER.clone();
tauri::async_runtime::spawn(async move {
let (mut rx, child) = Command::new_sidecar("qdrant")
@ -84,30 +95,6 @@ pub fn start_qdrant_server() {
});
}
/// This endpoint is called by the Qdrant server or frontend to signal that Qdrant is ready.
#[get("/system/qdrant/ready")]
pub async fn qdrant_ready(_token: APIToken) {
{
let mut initialized = QDRANT_INITIALIZED.lock().unwrap();
if *initialized {
warn!("Qdrant server was already initialized.");
return;
}
info!("Qdrant server is ready.");
*initialized = true;
}
let qdrant_port = *QDRANT_SERVER_PORT;
let _url = match Url::parse(format!("http://localhost:{qdrant_port}").as_str()) {
Ok(url) => url,
Err(msg) => {
error!("Error while parsing Qdrant URL: {msg}");
return;
}
};
}
/// Stops the Qdrant server process.
pub fn stop_qdrant_server() {
if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() {

View File

@ -67,6 +67,7 @@ pub fn start_runtime_api() {
.mount("/", routes![
crate::dotnet::dotnet_port,
crate::dotnet::dotnet_ready,
crate::qdrant::qdrant_port,
crate::clipboard::set_clipboard,
crate::app_window::get_event_stream,
crate::app_window::check_for_update,