diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 8d1d8de4..8cf531b8 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -173,6 +173,9 @@ jobs: pdfium_version=$(sed -n '11p' metadata.txt) pdfium_version=$(echo $pdfium_version | cut -d'.' -f3) + # Next line is the Qdrant version: + qdrant_version="v$(sed -n '12p' metadata.txt)" + # Write the metadata to the environment: echo "APP_VERSION=${app_version}" >> $GITHUB_ENV echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV @@ -185,6 +188,7 @@ jobs: echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV echo "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $GITHUB_ENV echo "PDFIUM_VERSION=${pdfium_version}" >> $GITHUB_ENV + echo "QDRANT_VERSION=${qdrant_version}" >> $GITHUB_ENV # Log the metadata: echo "App version: '${formatted_app_version}'" @@ -197,6 +201,7 @@ jobs: echo "Tauri version: '${tauri_version}'" echo "Architecture: '${{ matrix.dotnet_runtime }}'" echo "PDFium version: '${pdfium_version}'" + echo "Qdrant version: '${qdrant_version}'" - name: Read and format metadata (Windows) if: matrix.platform == 'windows-latest' @@ -241,6 +246,9 @@ jobs: $pdfium_version = $metadata[10] $pdfium_version = $pdfium_version.Split('.')[2] + # Next line is the necessary Qdrant version: + $qdrant_version = "v$metadata[12]" + # Write the metadata to the environment: Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV Write-Output "FORMATTED_APP_VERSION=${formatted_app_version}" >> $env:GITHUB_ENV @@ -252,6 +260,7 @@ jobs: Write-Output "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $env:GITHUB_ENV Write-Output "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $env:GITHUB_ENV Write-Output "PDFIUM_VERSION=${pdfium_version}" >> $env:GITHUB_ENV + Write-Output "QDRANT_VERSION=${qdrant_version}" >> $env:GITHUB_ENV # Log the metadata: Write-Output "App version: '${formatted_app_version}'" @@ -264,6 +273,7 @@ jobs: Write-Output "Tauri version: '${tauri_version}'" Write-Output "Architecture: '${{ matrix.dotnet_runtime }}'" Write-Output "PDFium version: '${pdfium_version}'" + Write-Output "Qdrant version: '${qdrant_version}'" - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -385,6 +395,121 @@ jobs: Write-Host "Cleaning up ..." Remove-Item $ARCHIVE -Force -ErrorAction SilentlyContinue + # Try to remove the temporary directory, but ignore errors if files are still in use + try { + Remove-Item $TMP -Recurse -Force -ErrorAction Stop + Write-Host "Successfully cleaned up temporary directory: $TMP" + } catch { + Write-Warning "Could not fully clean up temporary directory: $TMP. This is usually harmless as Windows will clean it up later. Error: $($_.Exception.Message)" + } + - name: Deploy Qdrant (Unix) + if: matrix.platform != 'windows-latest' + env: + QDRANT_VERSION: ${{ env.QDRANT_VERSION }} + DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }} + run: | + set -e + + # Target directory: + TDB_DIR="runtime/resources/databases/qdrant" + mkdir -p "$TDB_DIR" + + case "${DOTNET_RUNTIME}" in + linux-x64) + QDRANT_FILE="x86_64-unknown-linux-gnu.tar.gz" + DB_SOURCE="qdrant" + DB_TARGET="qdrant" + ;; + linux-arm64) + QDRANT_FILE="aarch64-unknown-linux-musl.tar.gz" + DB_SOURCE="qdrant" + DB_TARGET="qdrant" + ;; + osx-x64) + QDRANT_FILE="x86_64-apple-darwin.tar.gz" + DB_SOURCE="qdrant" + DB_TARGET="qdrant" + ;; + osx-arm64) + QDRANT_FILE="aarch64-apple-darwin.tar.gz" + DB_SOURCE="qdrant" + DB_TARGET="qdrant" + ;; + *) + echo "Unknown platform: ${DOTNET_RUNTIME}" + exit 1 + ;; + esac + + QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/v${QDRANT_VERSION}/qdrant-{QDRANT_FILE}" + + echo "Download Qdrant $QDRANT_URL ..." + TMP=$(mktemp -d) + ARCHIVE="${TMP}/qdrant.tgz" + + curl -fsSL -o "$ARCHIVE" "$QDRANT_URL" + + echo "Extracting Qdrant ..." + tar xzf "$ARCHIVE" -C "$TMP" + SRC="${TMP}/${DB_SOURCE}" + + if [ ! -f "$SRC" ]; then + echo "Was not able to find Qdrant source: $SRC" + exit 1 + fi + + echo "Copy Qdrant from ${DB_TARGET} to ${TDB_DIR}/" + cp -f "$SRC" "$TDB_DIR/$DB_TARGET" + + echo "Cleaning up ..." + rm -fr "$TMP" + + - name: Install Qdrant (Windows) + if: matrix.platform == 'windows-latest' + env: + QDRANT_VERSION: ${{ env.QDRANT_VERSION }} + DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }} + run: | + $TDB_DIR = "runtime\resources\databases\qdrant" + New-Item -ItemType Directory -Force -Path $TDB_DIR | Out-Null + + switch ($env:DOTNET_RUNTIME) { + "win-x64" { + $QDRANT_FILE = "x86_64-pc-windows-msvc.zip" + $DB_SOURCE = "qdrant.exe" + $DB_TARGET = "qdrant.exe" + } + default { + Write-Error "Unknown platform: $($env:DOTNET_RUNTIME)" + exit 1 + } + } + + 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) + $TMP = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) + New-Item -ItemType Directory -Path $TMP -Force | Out-Null + $ARCHIVE = Join-Path $TMP "qdrant.tgz" + + Invoke-WebRequest -Uri $QDRANT_URL -OutFile $ARCHIVE + + Write-Host "Extracting Qdrant ..." + tar -xzf $ARCHIVE -C $TMP + + $SRC = Join-Path $TMP $DB_SOURCE + if (!(Test-Path $SRC)) { + Write-Error "Cannot find Qdrant source: $SRC" + exit 1 + } + + $DEST = Join-Path $TDB_DIR $DB_TARGET + Copy-Item -Path $SRC -Destination $DEST -Force + + Write-Host "Cleaning up ..." + Remove-Item $ARCHIVE -Force -ErrorAction SilentlyContinue + # Try to remove the temporary directory, but ignore errors if files are still in use try { Remove-Item $TMP -Recurse -Force -ErrorAction Stop diff --git a/.gitignore b/.gitignore index 81a01256..cefdb845 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,13 @@ libpdfium.dylib libpdfium.so libpdfium.dll +# Ignore qdrant database: +qdrant-aarch64-apple-darwin +qdrant-x86_64-apple-darwin +qdrant-aarch64-unknown-linux-gnu +qdrant-x86_64-unknown-linux-gnu +qdrant-x86_64-pc-windows-msvc.exe + # User-specific files *.rsuser *.suo diff --git a/README.md b/README.md index d526d2b3..e8489aef 100644 --- a/README.md +++ b/README.md @@ -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))~~ diff --git a/app/Build/Commands/Database.cs b/app/Build/Commands/Database.cs new file mode 100644 index 00000000..dcd78391 --- /dev/null +++ b/app/Build/Commands/Database.cs @@ -0,0 +1,3 @@ +namespace Build.Commands; + +public record Database(string Path, string Filename); \ No newline at end of file diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs new file mode 100644 index 00000000..9a573823 --- /dev/null +++ b/app/Build/Commands/Qdrant.cs @@ -0,0 +1,119 @@ +using System.Diagnostics.Eventing.Reader; +using System.Formats.Tar; +using System.IO.Compression; + +using SharedTools; + +namespace Build.Commands; + +public static class Qdrant +{ + public static async Task InstallAsync(RID rid, string version) + { + Console.Write($"- Installing Qdrant {version} for {rid.ToUserFriendlyName()} ..."); + + var cwd = Environment.GetRustRuntimeDirectory(); + var qdrantTmpDownloadPath = Path.GetTempFileName(); + var qdrantTmpExtractPath = Directory.CreateTempSubdirectory(); + var qdrantUrl = GetQdrantDownloadUrl(rid, version); + + // + // Download the file: + // + Console.Write(" downloading ..."); + using (var client = new HttpClient()) + { + var response = await client.GetAsync(qdrantUrl); + if (!response.IsSuccessStatusCode) + { + Console.WriteLine($" failed to download Qdrant {version} for {rid.ToUserFriendlyName()} from {qdrantUrl}"); + return; + } + + await using var fileStream = File.Create(qdrantTmpDownloadPath); + await response.Content.CopyToAsync(fileStream); + } + + // + // Extract the downloaded file: + // + Console.Write(" extracting ..."); + await using(var zStream = File.Open(qdrantTmpDownloadPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + if (rid == RID.WIN_X64) + { + using var archive = new ZipArchive(zStream, ZipArchiveMode.Read); + archive.ExtractToDirectory(qdrantTmpExtractPath.FullName, overwriteFiles: true); + } else + { + await using var uncompressedStream = new GZipStream(zStream, CompressionMode.Decompress); + await TarFile.ExtractToDirectoryAsync(uncompressedStream, qdrantTmpExtractPath.FullName, true); + } + } + + // + // Copy the database to the target directory: + // + Console.Write(" deploying ..."); + var database = GetDatabasePath(rid); + if (string.IsNullOrWhiteSpace(database.Path)) + { + Console.WriteLine($" failed to find the database path for {rid.ToUserFriendlyName()}"); + return; + } + + var qdrantDBSourcePath = Path.Join(qdrantTmpExtractPath.FullName, database.Path); + var qdrantDBTargetPath = Path.Join(cwd, "resources", "databases", "qdrant",database.Filename); + if (!File.Exists(qdrantDBSourcePath)) + { + Console.WriteLine($" failed to find the database file '{qdrantDBSourcePath}'"); + return; + } + + Directory.CreateDirectory(Path.Join(cwd, "resources", "databases", "qdrant")); + if (File.Exists(qdrantDBTargetPath)) + File.Delete(qdrantDBTargetPath); + + File.Copy(qdrantDBSourcePath, qdrantDBTargetPath); + + // + // Cleanup: + // + Console.Write(" cleaning up ..."); + File.Delete(qdrantTmpDownloadPath); + Directory.Delete(qdrantTmpExtractPath.FullName, true); + + Console.WriteLine(" done."); + } + + private static Database GetDatabasePath(RID rid) => rid switch + { + RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"), + RID.OSX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"), + + 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"), + + _ => new(string.Empty, string.Empty), + }; + + private static string GetQdrantDownloadUrl(RID rid, string version) + { + 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", + RID.LINUX_X64 => $"{baseUrl}x86_64-unknown-linux-gnu.tar.gz", + + RID.OSX_ARM64 => $"{baseUrl}aarch64-apple-darwin.tar.gz", + RID.OSX_X64 => $"{baseUrl}x86_64-apple-darwin.tar.gz", + + RID.WIN_X64 => $"{baseUrl}x86_64-pc-windows-msvc.zip", + #warning We have to handle Qdrant for Windows ARM + + _ => string.Empty, + }; + } +} \ No newline at end of file diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs index 06910b45..cc5b6783 100644 --- a/app/Build/Commands/UpdateMetadataCommands.cs +++ b/app/Build/Commands/UpdateMetadataCommands.cs @@ -112,6 +112,9 @@ public async Task Build() var pdfiumVersion = await this.ReadPdfiumVersion(); await Pdfium.InstallAsync(rid, pdfiumVersion); + + var qdrantVersion = await this.ReadQdrantVersion(); + await Qdrant.InstallAsync(rid, qdrantVersion); Console.Write($"- Start .NET build for {rid.ToUserFriendlyName()} ..."); await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}"); @@ -324,6 +327,16 @@ private async Task ReadPdfiumVersion() return shortVersion; } + private async Task ReadQdrantVersion() + { + const int QDRANT_VERSION_INDEX = 11; + var pathMetadata = Environment.GetMetadataPath(); + var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8); + var currentQdrantVersion = lines[QDRANT_VERSION_INDEX].Trim(); + + return currentQdrantVersion; + } + private async Task UpdateArchitecture(RID rid) { const int ARCHITECTURE_INDEX = 9; diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 74d4cc67..8a4bd3d9 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4510,6 +4510,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." @@ -4549,6 +4552,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" @@ -4723,6 +4729,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T855925638"] = "We use this library to -- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T870640199"] = "For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose." +-- Qdrant is a vector similarity search engine and vector database. It provides a production-ready service with a convenient API to store, search, and manage points—vectors with an additional payload Qdrant is tailored to extended filtering support. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T95576615"] = "Qdrant is a vector similarity search engine and vector database. It provides a production-ready service with a convenient API to store, search, and manage points—vectors with an additional payload Qdrant is tailored to extended filtering support." + -- Install Pandoc UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T986578435"] = "Install Pandoc" diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index b559389c..b4b16cd2 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -87,6 +87,7 @@ $([System.String]::Copy( $(Metadata) ).Split( ';' )[ 8 ]) $([System.String]::Copy( $(Metadata) ).Split( ';' )[ 9 ]) $([System.String]::Copy( $(Metadata) ).Split( ';' )[ 10 ]) + $([System.String]::Copy( $(Metadata) ).Split( ';' )[ 11 ]) true @@ -114,6 +115,9 @@ <_Parameter1>$(MetaPdfiumVersion) + + <_Parameter1>$(MetaQdrantVersion) + diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor index cf66b900..d9fd748b 100644 --- a/app/MindWork AI Studio/Pages/About.razor +++ b/app/MindWork AI Studio/Pages/About.razor @@ -19,6 +19,29 @@ + + + @this.VersionDatabase + + + + @foreach (var (Label, Value) in DatabaseClient.GetDisplayInfo()) + { +
+ + @Label: @Value + +
+ } +
+
+ + @(this.showDatabaseDetails ? T("Hide Details") : T("Show Details")) + +
@@ -194,6 +217,7 @@ + diff --git a/app/MindWork AI Studio/Pages/About.razor.cs b/app/MindWork AI Studio/Pages/About.razor.cs index ecdf1d17..9371c610 100644 --- a/app/MindWork AI Studio/Pages/About.razor.cs +++ b/app/MindWork AI Studio/Pages/About.razor.cs @@ -2,6 +2,7 @@ using AIStudio.Components; using AIStudio.Dialogs; +using AIStudio.Tools.Databases; using AIStudio.Tools.Metadata; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; @@ -26,10 +27,14 @@ 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()!; private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute()!; private static readonly MetaDataLibrariesAttribute META_DATA_LIBRARIES = ASSEMBLY.GetCustomAttribute()!; + private static readonly MetaDataDatabasesAttribute META_DATA_DATABASES = ASSEMBLY.GetCustomAttribute()!; private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(About).Namespace, nameof(About)); @@ -53,6 +58,8 @@ public partial class About : MSGComponentBase private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}"; + 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; @@ -60,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); /// @@ -170,6 +179,11 @@ private void ToggleEnterpriseConfigDetails() { this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails; } + + private void ToggleDatabaseDetails() + { + this.showDatabaseDetails = !this.showDatabaseDetails; + } private async Task CopyStartupLogPath() { diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index b5954efc..0b63f17a 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -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 @@ public static async Task Main() 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 => @@ -134,6 +155,7 @@ public static async Task Main() builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); + builder.Services.AddSingleton(new QdrantClient("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc)); // ReSharper disable AccessToDisposedClosure builder.Services.AddHostedService(_ => rust); @@ -216,6 +238,7 @@ public static async Task Main() 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"}'."); diff --git a/app/MindWork AI Studio/Settings/Profile.cs b/app/MindWork AI Studio/Settings/Profile.cs index 0436beb5..2e9dc80a 100644 --- a/app/MindWork AI Studio/Settings/Profile.cs +++ b/app/MindWork AI Studio/Settings/Profile.cs @@ -77,7 +77,6 @@ The user wants you to consider the following things. 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(out var idText) || !Guid.TryParse(idText, out var id)) { diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs new file mode 100644 index 00000000..0ca84e01 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs @@ -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(); + 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]}"; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClient.cs b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClient.cs new file mode 100644 index 00000000..c3a4fabd --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClient.cs @@ -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()}"); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs b/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs new file mode 100644 index 00000000..5ef6064b --- /dev/null +++ b/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs @@ -0,0 +1,6 @@ +namespace AIStudio.Tools.Metadata; + +public class MetaDataDatabasesAttribute(string databaseVersion) : Attribute +{ + public string DatabaseVersion => databaseVersion; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs new file mode 100644 index 00000000..8cbe5e9c --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs @@ -0,0 +1,13 @@ +namespace AIStudio.Tools.Rust; + +/// +/// The response of the Qdrant information request. +/// +/// The port number for HTTP communication with Qdrant. +/// The port number for gRPC communication with Qdrant +public record struct QdrantInfo +{ + public string Path { get; init; } + public int PortHttp { get; init; } + public int PortGrpc { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs new file mode 100644 index 00000000..ae42316d --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs @@ -0,0 +1,26 @@ +using AIStudio.Tools.Rust; + +namespace AIStudio.Tools.Services; + +public sealed partial class RustService +{ + public async Task GetQdrantInfo() + { + try + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45)); + var response = await this.http.GetFromJsonAsync("/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, + }; + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md index 79563fd1..a47827bb 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md @@ -1,4 +1,5 @@ # v0.9.55, build 230 (2025-12-xx xx:xx UTC) +- Added functionality to download Qdrant and execute it as a background sidecar. - Added support for newer Mistral models (Mistral 3, Voxtral, and Magistral). - Added a description field to local data sources (preview feature) so that the data selection agent has more information about which data each local source contains when selecting data sources. - Added the ability to use file attachments in chat. This is the initial implementation of this feature. We will continue to develop this feature and refine it further based on user feedback. Many thanks to Sabrina `Sabrina-devops` for this wonderful contribution. @@ -9,4 +10,4 @@ - Improved error handling for Microsoft Word export. - Fixed a bug in the local data sources info dialog (preview feature) for data directories that could cause the app to crash. The error was caused by a background thread producing data while the frontend attempted to display it. - Fixed a visual bug where a function's preview status was misaligned. You might have seen it in document analysis or the ERI server assistant. -- Fixed a rare bug in the Microsoft Word export for huge documents. \ No newline at end of file +- Fixed a rare bug in the Microsoft Word export for huge documents. diff --git a/metadata.txt b/metadata.txt index 8b62056f..5746ea97 100644 --- a/metadata.txt +++ b/metadata.txt @@ -8,4 +8,5 @@ 1.8.1 009bb33d839, release osx-arm64 -137.0.7215.0 \ No newline at end of file +137.0.7215.0 +1.16.1 \ No newline at end of file diff --git a/runtime/resources/databases/qdrant/config.yaml b/runtime/resources/databases/qdrant/config.yaml new file mode 100644 index 00000000..267f81c2 --- /dev/null +++ b/runtime/resources/databases/qdrant/config.yaml @@ -0,0 +1,353 @@ +log_level: INFO + +# Logging configuration +# Qdrant logs to stdout. You may configure to also write logs to a file on disk. +# Be aware that this file may grow indefinitely. +# logger: +# # Logging format, supports `text` and `json` +# format: text +# on_disk: +# enabled: true +# log_file: path/to/log/file.log +# log_level: INFO +# # Logging format, supports `text` and `json` +# format: text +# buffer_size_bytes: 1024 + +storage: + + snapshots_config: + # "local" or "s3" - where to store snapshots + snapshots_storage: local + # s3_config: + # bucket: "" + # region: "" + # access_key: "" + # secret_key: "" + + # Where to store temporary files + # If null, temporary snapshots are stored in: storage/snapshots_temp/ + temp_path: null + + # If true - point payloads will not be stored in memory. + # It will be read from the disk every time it is requested. + # This setting saves RAM by (slightly) increasing the response time. + # Note: those payload values that are involved in filtering and are indexed - remain in RAM. + # + # Default: true + on_disk_payload: true + + # Maximum number of concurrent updates to shard replicas + # If `null` - maximum concurrency is used. + update_concurrency: null + + # Write-ahead-log related configuration + wal: + # Size of a single WAL segment + wal_capacity_mb: 32 + + # Number of WAL segments to create ahead of actual data requirement + wal_segments_ahead: 0 + + # Normal node - receives all updates and answers all queries + node_type: "Normal" + + # Listener node - receives all updates, but does not answer search/read queries + # Useful for setting up a dedicated backup node + # node_type: "Listener" + + performance: + # Number of parallel threads used for search operations. If 0 - auto selection. + max_search_threads: 0 + + # CPU budget, how many CPUs (threads) to allocate for an optimization job. + # If 0 - auto selection, keep 1 or more CPUs unallocated depending on CPU size + # If negative - subtract this number of CPUs from the available CPUs. + # If positive - use this exact number of CPUs. + optimizer_cpu_budget: 0 + + # Prevent DDoS of too many concurrent updates in distributed mode. + # One external update usually triggers multiple internal updates, which breaks internal + # timings. For example, the health check timing and consensus timing. + # If null - auto selection. + update_rate_limit: null + + # Limit for number of incoming automatic shard transfers per collection on this node, does not affect user-requested transfers. + # The same value should be used on all nodes in a cluster. + # Default is to allow 1 transfer. + # If null - allow unlimited transfers. + #incoming_shard_transfers_limit: 1 + + # Limit for number of outgoing automatic shard transfers per collection on this node, does not affect user-requested transfers. + # The same value should be used on all nodes in a cluster. + # Default is to allow 1 transfer. + # If null - allow unlimited transfers. + #outgoing_shard_transfers_limit: 1 + + # Enable async scorer which uses io_uring when rescoring. + # Only supported on Linux, must be enabled in your kernel. + # See: + #async_scorer: false + + optimizers: + # The minimal fraction of deleted vectors in a segment, required to perform segment optimization + deleted_threshold: 0.2 + + # The minimal number of vectors in a segment, required to perform segment optimization + vacuum_min_vector_number: 1000 + + # Target amount of segments optimizer will try to keep. + # Real amount of segments may vary depending on multiple parameters: + # - Amount of stored points + # - Current write RPS + # + # It is recommended to select default number of segments as a factor of the number of search threads, + # so that each segment would be handled evenly by one of the threads. + # If `default_segment_number = 0`, will be automatically selected by the number of available CPUs + default_segment_number: 0 + + # Do not create segments larger this size (in KiloBytes). + # Large segments might require disproportionately long indexation times, + # therefore it makes sense to limit the size of segments. + # + # If indexation speed have more priority for your - make this parameter lower. + # If search speed is more important - make this parameter higher. + # Note: 1Kb = 1 vector of size 256 + # If not set, will be automatically selected considering the number of available CPUs. + max_segment_size_kb: null + + # Maximum size (in KiloBytes) of vectors allowed for plain index. + # Default value based on experiments and observations. + # Note: 1Kb = 1 vector of size 256 + # To explicitly disable vector indexing, set to `0`. + # If not set, the default value will be used. + indexing_threshold_kb: 10000 + + # Interval between forced flushes. + flush_interval_sec: 5 + + # Max number of threads (jobs) for running optimizations per shard. + # Note: each optimization job will also use `max_indexing_threads` threads by itself for index building. + # If null - have no limit and choose dynamically to saturate CPU. + # If 0 - no optimization threads, optimizations will be disabled. + max_optimization_threads: null + + # This section has the same options as 'optimizers' above. All values specified here will overwrite the collections + # optimizers configs regardless of the config above and the options specified at collection creation. + #optimizers_overwrite: + # deleted_threshold: 0.2 + # vacuum_min_vector_number: 1000 + # default_segment_number: 0 + # max_segment_size_kb: null + # indexing_threshold_kb: 10000 + # flush_interval_sec: 5 + # max_optimization_threads: null + + # Default parameters of HNSW Index. Could be overridden for each collection or named vector individually + hnsw_index: + # Number of edges per node in the index graph. Larger the value - more accurate the search, more space required. + m: 16 + + # Number of neighbours to consider during the index building. Larger the value - more accurate the search, more time required to build index. + ef_construct: 100 + + # Minimal size threshold (in KiloBytes) below which full-scan is preferred over HNSW search. + # This measures the total size of vectors being queried against. + # When the maximum estimated amount of points that a condition satisfies is smaller than + # `full_scan_threshold_kb`, the query planner will use full-scan search instead of HNSW index + # traversal for better performance. + # Note: 1Kb = 1 vector of size 256 + full_scan_threshold_kb: 10000 + + # Number of parallel threads used for background index building. + # If 0 - automatically select. + # Best to keep between 8 and 16 to prevent likelihood of building broken/inefficient HNSW graphs. + # On small CPUs, less threads are used. + max_indexing_threads: 0 + + # Store HNSW index on disk. If set to false, index will be stored in RAM. Default: false + on_disk: false + + # Custom M param for hnsw graph built for payload index. If not set, default M will be used. + payload_m: null + + # Default shard transfer method to use if none is defined. + # If null - don't have a shard transfer preference, choose automatically. + # If stream_records, snapshot or wal_delta - prefer this specific method. + # More info: https://qdrant.tech/documentation/guides/distributed_deployment/#shard-transfer-method + shard_transfer_method: null + + # Default parameters for collections + collection: + # Number of replicas of each shard that network tries to maintain + replication_factor: 1 + + # How many replicas should apply the operation for us to consider it successful + write_consistency_factor: 1 + + # Default parameters for vectors. + vectors: + # Whether vectors should be stored in memory or on disk. + on_disk: null + + # shard_number_per_node: 1 + + # Default quantization configuration. + # More info: https://qdrant.tech/documentation/guides/quantization + quantization: null + + # Default strict mode parameters for newly created collections. + #strict_mode: + # Whether strict mode is enabled for a collection or not. + #enabled: false + + # Max allowed `limit` parameter for all APIs that don't have their own max limit. + #max_query_limit: null + + # Max allowed `timeout` parameter. + #max_timeout: null + + # Allow usage of unindexed fields in retrieval based (eg. search) filters. + #unindexed_filtering_retrieve: null + + # Allow usage of unindexed fields in filtered updates (eg. delete by payload). + #unindexed_filtering_update: null + + # Max HNSW value allowed in search parameters. + #search_max_hnsw_ef: null + + # Whether exact search is allowed or not. + #search_allow_exact: null + + # Max oversampling value allowed in search. + #search_max_oversampling: null + + # Maximum number of collections allowed to be created + # If null - no limit. + max_collections: null + +service: + # Maximum size of POST data in a single request in megabytes + max_request_size_mb: 32 + + # Number of parallel workers used for serving the api. If 0 - equal to the number of available cores. + # If missing - Same as storage.max_search_threads + max_workers: 0 + + # Host to bind the service on + host: 127.0.0.1 + + # HTTP(S) port to bind the service on + # http_port: 6333 + + # gRPC port to bind the service on. + # If `null` - gRPC is disabled. Default: null + # Comment to disable gRPC: + # grpc_port: 6334 + + # Enable CORS headers in REST API. + # If enabled, browsers would be allowed to query REST endpoints regardless of query origin. + # More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS + # Default: true + enable_cors: true + + # Enable HTTPS for the REST and gRPC API + enable_tls: false + + # Check user HTTPS client certificate against CA file specified in tls config + verify_https_client_certificate: false + + # Set an api-key. + # If set, all requests must include a header with the api-key. + # example header: `api-key: ` + # + # If you enable this you should also enable TLS. + # (Either above or via an external service like nginx.) + # Sending an api-key over an unencrypted channel is insecure. + # + # Uncomment to enable. + # api_key: your_secret_api_key_here + + # Set an api-key for read-only operations. + # If set, all requests must include a header with the api-key. + # example header: `api-key: ` + # + # If you enable this you should also enable TLS. + # (Either above or via an external service like nginx.) + # Sending an api-key over an unencrypted channel is insecure. + # + # Uncomment to enable. + # read_only_api_key: your_secret_read_only_api_key_here + + # Uncomment to enable JWT Role Based Access Control (RBAC). + # If enabled, you can generate JWT tokens with fine-grained rules for access control. + # Use generated token instead of API key. + # + # jwt_rbac: true + + # Hardware reporting adds information to the API responses with a + # hint on how many resources were used to execute the request. + # + # Warning: experimental, this feature is still under development and is not supported yet. + # + # Uncomment to enable. + # hardware_reporting: true + # + # Uncomment to enable. + # Prefix for the names of metrics in the /metrics API. + # metrics_prefix: qdrant_ + +cluster: + # Use `enabled: true` to run Qdrant in distributed deployment mode + enabled: false + + # Configuration of the inter-cluster communication + p2p: + # Port for internal communication between peers + port: 6335 + + # Use TLS for communication between peers + enable_tls: false + + # Configuration related to distributed consensus algorithm + consensus: + # How frequently peers should ping each other. + # Setting this parameter to lower value will allow consensus + # to detect disconnected nodes earlier, but too frequent + # tick period may create significant network and CPU overhead. + # We encourage you NOT to change this parameter unless you know what you are doing. + tick_period_ms: 100 + + # Compact consensus operations once we have this amount of applied + # operations. Allows peers to join quickly with a consensus snapshot without + # replaying a huge amount of operations. + # If 0 - disable compaction + compact_wal_entries: 128 + +# Set to true to prevent service from sending usage statistics to the developers. +# Read more: https://qdrant.tech/documentation/guides/telemetry +telemetry_disabled: true + +# TLS configuration. +# Required if either service.enable_tls or cluster.p2p.enable_tls is true. +tls: + # Server certificate chain file + cert: ./tls/cert.pem + + # Server private key file + key: ./tls/key.pem + + # Certificate authority certificate file. + # This certificate will be used to validate the certificates + # presented by other nodes during inter-cluster communication. + # + # If verify_https_client_certificate is true, it will verify + # HTTPS client certificate + # + # Required if cluster.p2p.enable_tls is true. + ca_cert: ./tls/cacert.pem + + # TTL in seconds to reload certificate from disk, useful for certificate rotations. + # Only works for HTTPS endpoints. Does not support gRPC (and intra-cluster communication). + # If `null` - TTL is disabled. + cert_ttl: 3600 \ No newline at end of file diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index dd994415..7cd97b8b 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -17,6 +17,7 @@ use crate::dotnet::stop_dotnet_server; use crate::environment::{is_prod, is_dev, CONFIG_DIRECTORY, DATA_DIRECTORY}; use crate::log::switch_to_file_logging; use crate::pdfium::PDFIUM_LIB_PATH; +use crate::qdrant::start_qdrant_server; /// The Tauri main window. static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)); @@ -94,6 +95,9 @@ pub fn start_tauri() { info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}"); switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap(); set_pdfium_path(app.path_resolver()); + + start_qdrant_server(); + Ok(()) }) .plugin(tauri_plugin_window_state::Builder::default().build()) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7868a7a4..bd7da307 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -12,4 +12,5 @@ pub mod certificate; pub mod file_data; pub mod metadata; pub mod pdfium; -pub mod pandoc; \ No newline at end of file +pub mod pandoc; +pub mod qdrant; \ No newline at end of file diff --git a/runtime/src/main.rs b/runtime/src/main.rs index a66ee287..bfbe4750 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -38,6 +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: v{qdrant_version}", qdrant_version = metadata.qdrant_version); if is_dev() { warn!("Running in development mode."); diff --git a/runtime/src/metadata.rs b/runtime/src/metadata.rs index 426e2b66..fa56dd68 100644 --- a/runtime/src/metadata.rs +++ b/runtime/src/metadata.rs @@ -16,6 +16,7 @@ pub struct MetaData { pub app_commit_hash: String, pub architecture: String, pub pdfium_version: String, + pub qdrant_version: String, } impl MetaData { @@ -39,6 +40,7 @@ impl MetaData { let app_commit_hash = metadata_lines.next().unwrap(); let architecture = metadata_lines.next().unwrap(); let pdfium_version = metadata_lines.next().unwrap(); + let qdrant_version = metadata_lines.next().unwrap(); let metadata = MetaData { architecture: architecture.to_string(), @@ -52,6 +54,7 @@ impl MetaData { rust_version: rust_version.to_string(), tauri_version: tauri_version.to_string(), pdfium_version: pdfium_version.to_string(), + qdrant_version: qdrant_version.to_string(), }; *META_DATA.lock().unwrap() = Some(metadata.clone()); diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs new file mode 100644 index 00000000..3b2b94ce --- /dev/null +++ b/runtime/src/qdrant.rs @@ -0,0 +1,109 @@ +use std::collections::HashMap; +use std::path::Path; +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 crate::api_token::{APIToken}; +use crate::environment::DATA_DIRECTORY; + +// Qdrant server process started in a separate process and can communicate +// via HTTP or gRPC with the .NET server and the runtime process +static QDRANT_SERVER: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); + +// Qdrant server port (default is 6333 for HTTP and 6334 for gRPC) +static QDRANT_SERVER_PORT_HTTP: Lazy = Lazy::new(|| { + crate::network::get_available_port().unwrap_or(6333) +}); + +static QDRANT_SERVER_PORT_GRPC: Lazy = 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) -> Json { + 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. +pub fn start_qdrant_server() { + + let base_path = DATA_DIRECTORY.get().unwrap(); + + let storage_path = Path::new(base_path).join("databases").join("qdrant").join("storage").to_str().unwrap().to_string(); + 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(); + + let qdrant_server_environment = HashMap::from_iter([ + (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") + .expect("Failed to create sidecar for Qdrant") + .args(["--config-path", "resources/databases/qdrant/config.yaml"]) + .envs(qdrant_server_environment) + .spawn() + .expect("Failed to spawn Qdrant server process."); + + let server_pid = child.pid(); + info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}."); + + // Save the server process to stop it later: + *server_spawn_clone.lock().unwrap() = Some(child); + + // Log the output of the Qdrant server: + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(line) => { + let line = line.trim_end(); + if line.contains("INFO") || line.contains("info") { + info!(Source = "Qdrant Server"; "{line}"); + } else if line.contains("WARN") || line.contains("warning") { + warn!(Source = "Qdrant Server"; "{line}"); + } else if line.contains("ERROR") || line.contains("error") { + error!(Source = "Qdrant Server"; "{line}"); + } else { + debug!(Source = "Qdrant Server"; "{line}"); + } + } + CommandEvent::Stderr(line) => { + error!(Source = "Qdrant Server (stderr)"; "{line}"); + } + _ => {} + } + } + }); +} + +/// Stops the Qdrant server process. +pub fn stop_qdrant_server() { + if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() { + let server_kill_result = server_process.kill(); + match server_kill_result { + Ok(_) => info!("Qdrant server process was stopped."), + Err(e) => error!("Failed to stop Qdrant server process: {e}."), + } + } else { + warn!("Qdrant server process was not started or is already stopped."); + } +} \ No newline at end of file diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 23fc5e33..529d9636 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -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, diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index ef116add..d2cb54f3 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -20,6 +20,11 @@ "name": "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", "sidecar": true, "args": true + }, + { + "name": "resources/databases/qdrant/qdrant", + "sidecar": true, + "args": true } ] }, @@ -59,7 +64,8 @@ "targets": "all", "identifier": "com.github.mindwork-ai.ai-studio", "externalBin": [ - "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer" + "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", + "resources/databases/qdrant/qdrant" ], "resources": [ "resources/*"