diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 3bd6ddf9..c39b90e0 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -329,8 +329,8 @@ 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)" + # Next line is the vector store version: + vector_store_version="$(sed -n '12p' metadata.txt)" # Write the metadata to the environment: echo "APP_VERSION=${app_version}" >> $GITHUB_ENV @@ -344,7 +344,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 + echo "VECTOR_STORE_VERSION=${vector_store_version}" >> $GITHUB_ENV # Log the metadata: echo "App version: '${formatted_app_version}'" @@ -357,7 +357,7 @@ jobs: echo "Tauri version: '${tauri_version}'" echo "Architecture: '${{ matrix.dotnet_runtime }}'" echo "PDFium version: '${pdfium_version}'" - echo "Qdrant version: '${qdrant_version}'" + echo "Vector store version: '${vector_store_version}'" - name: Read and format metadata (Windows) if: matrix.platform == 'windows-latest' @@ -402,8 +402,8 @@ jobs: $pdfium_version = $metadata[10] $pdfium_version = $pdfium_version.Split('.')[2] - # Next line is the necessary Qdrant version: - $qdrant_version = "v$($metadata[11])" + # Next line is the vector store version: + $vector_store_version = $metadata[11] # Write the metadata to the environment: Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV @@ -416,7 +416,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 + Write-Output "VECTOR_STORE_VERSION=${vector_store_version}" >> $env:GITHUB_ENV # Log the metadata: Write-Output "App version: '${formatted_app_version}'" @@ -429,7 +429,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}'" + Write-Output "Vector store version: '${vector_store_version}'" - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -558,129 +558,6 @@ jobs: } 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 }} - RUST_TARGET: ${{ matrix.rust_target }} - run: | - set -e - - # Target directory: - TDB_DIR="runtime/target/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-${RUST_TARGET}" - ;; - linux-arm64) - QDRANT_FILE="aarch64-unknown-linux-musl.tar.gz" - DB_SOURCE="qdrant" - DB_TARGET="qdrant-${RUST_TARGET}" - ;; - osx-x64) - QDRANT_FILE="x86_64-apple-darwin.tar.gz" - DB_SOURCE="qdrant" - DB_TARGET="qdrant-${RUST_TARGET}" - ;; - osx-arm64) - QDRANT_FILE="aarch64-apple-darwin.tar.gz" - DB_SOURCE="qdrant" - DB_TARGET="qdrant-${RUST_TARGET}" - ;; - *) - echo "Unknown platform: ${DOTNET_RUNTIME}" - exit 1 - ;; - esac - - QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${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: Deploy Qdrant (Windows) - if: matrix.platform == 'windows-latest' - env: - QDRANT_VERSION: ${{ env.QDRANT_VERSION }} - DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }} - RUST_TARGET: ${{ matrix.rust_target }} - run: | - $TDB_DIR = "runtime\target\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-$($env:RUST_TARGET).exe" - } - "win-arm64" { - $QDRANT_FILE = "x86_64-pc-windows-msvc.zip" - $DB_SOURCE = "qdrant.exe" - $DB_TARGET = "qdrant-$($env:RUST_TARGET).exe" - } - default { - Write-Error "Unknown platform: $($env:DOTNET_RUNTIME)" - exit 1 - } - } - - $QDRANT_URL = "https://github.com/qdrant/qdrant/releases/download/$($env: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 - 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: Build .NET project run: | cd "app/MindWork AI Studio" diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs deleted file mode 100644 index 29369ccf..00000000 --- a/app/Build/Commands/Qdrant.cs +++ /dev/null @@ -1,120 +0,0 @@ -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, "target", "databases", "qdrant",database.Filename); - if (!File.Exists(qdrantDbSourcePath)) - { - Console.WriteLine($" failed to find the database file '{qdrantDbSourcePath}'"); - return; - } - - Directory.CreateDirectory(Path.Join(cwd, "target", "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"), - RID.WIN_ARM64 => new("qdrant.exe", "qdrant-aarch64-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", - RID.WIN_ARM64 => $"{baseUrl}x86_64-pc-windows-msvc.zip", - - _ => string.Empty, - }; - } -} \ No newline at end of file diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs index f3b0799e..303edcd5 100644 --- a/app/Build/Commands/UpdateMetadataCommands.cs +++ b/app/Build/Commands/UpdateMetadataCommands.cs @@ -69,6 +69,7 @@ public sealed partial class UpdateMetadataCommands await this.UpdateRustVersion(); await this.UpdateMudBlazorVersion(); await this.UpdateTauriVersion(); + await this.UpdateVectorStoreVersion(); } [Command("prepare", Description = "Prepare the metadata for the next release")] @@ -126,6 +127,7 @@ public sealed partial class UpdateMetadataCommands await this.UpdateRustVersion(); await this.UpdateMudBlazorVersion(); await this.UpdateTauriVersion(); + await this.UpdateVectorStoreVersion(); await this.UpdateProjectCommitHash(); await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "..", "..", "LICENSE.md"))); await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "Information.razor.cs"))); @@ -147,12 +149,11 @@ public sealed partial class UpdateMetadataCommands Console.WriteLine("=============================="); await this.UpdateArchitecture(rid); + await this.UpdateTauriVersion(); + await this.UpdateVectorStoreVersion(); 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()}"); @@ -367,16 +368,6 @@ public sealed partial class UpdateMetadataCommands 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; @@ -529,7 +520,32 @@ public sealed partial class UpdateMetadataCommands await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM); } - + + private async Task UpdateVectorStoreVersion() + { + const int VECTOR_STORE_VERSION_INDEX = 11; + + var pathMetadata = Environment.GetMetadataPath(); + var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8); + var currentVectorStoreVersion = lines[VECTOR_STORE_VERSION_INDEX].Trim(); + + var matches = await this.DetermineVersion("Qdrant Edge", Environment.GetRustRuntimeDirectory(), QdrantEdgeVersionRegex(), "cargo", "tree --depth 1"); + if (matches.Count == 0) + return; + + var updatedVectorStoreVersion = matches[0].Groups["version"].Value; + if(currentVectorStoreVersion == updatedVectorStoreVersion) + { + Console.WriteLine("- The vector store version is already up to date."); + return; + } + + Console.WriteLine($"- Updated vector store version from {currentVectorStoreVersion} to {updatedVectorStoreVersion}."); + lines[VECTOR_STORE_VERSION_INDEX] = updatedVectorStoreVersion; + + await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM); + } + private async Task UpdateMudBlazorVersion() { const int MUD_BLAZOR_VERSION_INDEX = 6; @@ -720,6 +736,9 @@ public sealed partial class UpdateMetadataCommands [GeneratedRegex("""MudBlazor\s+(?[0-9.]+)""")] private static partial Regex MudBlazorVersionRegex(); + [GeneratedRegex("""qdrant-edge\s+v(?[0-9.]+)""")] + private static partial Regex QdrantEdgeVersionRegex(); + [GeneratedRegex("""tauri\s+v(?[0-9.]+)""")] private static partial Regex TauriVersionRegex(); diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 1f0bc6f0..99257cc7 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -6076,6 +6076,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T103551060"] = "The configured ro -- 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" + +-- Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1126023000"] = "Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant." + -- 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." @@ -6094,9 +6100,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Copies the confi -- 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." @@ -6112,9 +6115,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Encryption secre -- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active." --- Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1619832053"] = "Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant." - -- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library." @@ -6163,6 +6163,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" +-- This library is used to create temporary folders in runtime tests and supporting filesystem operations. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "This library is used to create temporary folders in runtime tests and supporting filesystem operations." + -- This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2173617769"] = "This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file." @@ -6214,9 +6217,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time" -- unknown UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2608177081"] = "unknown" --- This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2619858133"] = "This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant." - -- This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems." @@ -6271,6 +6271,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog" -- External HTTPS custom root certificates are configured but not active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "External HTTPS custom root certificates are configured but not active." +-- Vector store +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store" + -- Enterprise configuration ID: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3092349641"] = "Enterprise configuration ID:" @@ -6376,9 +6379,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" - -- Allowed hosts: none configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4058524336"] = "Allowed hosts: none configured" @@ -7129,20 +7129,32 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = " -- Status UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" --- Storage size -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1093747001"] = "Reason" --- HTTP port -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP port" +-- Starting +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1233211769"] = "Starting" + +-- 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::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T1230141403"] = "Storage size" + +-- Number of vector stores +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T2785004838"] = "Number of vector stores" -- Reported version -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Reported version" +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T3556099842"] = "Reported version" --- gRPC port -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC port" +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T6222351"] = "Status" --- Number of collections -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Number of collections" +-- Qdrant Edge is not available. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge is not available." -- 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." diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index 7cebafb9..a2247811 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -53,7 +53,6 @@ - @@ -88,7 +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 ]) + $([System.String]::Copy( $(Metadata) ).Split( ';' )[ 11 ]) true @@ -116,8 +115,8 @@ <_Parameter1>$(MetaPdfiumVersion) - - <_Parameter1>$(MetaQdrantVersion) + + <_Parameter1>$(MetaVectorStoreVersion) diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 3d408d8c..ac3df15e 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -21,11 +21,11 @@ - @this.VersionDatabase + @this.VersionVectorStore - + - @foreach (var item in this.databaseDisplayInfo) + @foreach (var item in this.vectorStoreDisplayInfo) {
@@ -35,11 +35,11 @@ } - - @(this.showDatabaseDetails ? T("Hide Details") : T("Show Details")) + OnClick="@this.ToggleVectorStoreDetails"> + @(this.showVectorStoreDetails ? T("Hide Details") : T("Show Details")) @@ -289,7 +289,7 @@ } - + @@ -314,7 +314,7 @@ - + diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 45d21d8b..21fe274c 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -4,6 +4,7 @@ using AIStudio.Components; using AIStudio.Dialogs; using AIStudio.Settings.DataModel; using AIStudio.Tools.Databases; +using AIStudio.Tools.Databases.VectorStore; using AIStudio.Tools.Metadata; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; @@ -35,7 +36,7 @@ public partial class Information : MSGComponentBase 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 readonly MetaDataVectorStoreAttribute META_DATA_VECTOR_STORE = ASSEMBLY.GetCustomAttribute()!; private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Information).Namespace, nameof(Information)); @@ -77,18 +78,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")}" }; } } @@ -100,10 +101,9 @@ public partial class Information : MSGComponentBase private bool showEnterpriseConfigDetails; + private bool showVectorStoreDetails; private bool showExternalHttpCustomRootCertificateDetails; - private bool showDatabaseDetails; - private List configPlugins = PluginFactory.AvailablePlugins .Where(x => x.Type is PluginType.CONFIGURATION) .OfType() @@ -112,14 +112,13 @@ public partial class Information : MSGComponentBase private List enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList(); private List 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 = new(); - private DatabaseClient? databaseClient; - private CancellationTokenSource? databaseRefreshCancellationTokenSource; + + private sealed record VectorStoreDisplayInfo(string Label, string Value); + private readonly List vectorStoreDisplayInfo = new(); + private DatabaseClient? vectorStore; + private CancellationTokenSource? vectorStoreRefreshCancellationTokenSource; private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive); @@ -166,9 +165,9 @@ public partial class Information : MSGComponentBase this.runtimeInfo = await this.RustService.GetRuntimeInfo(); 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: @@ -272,22 +271,22 @@ public partial class Information : MSGComponentBase this.showExternalHttpCustomRootCertificateDetails = !this.showExternalHttpCustomRootCertificateDetails; } - 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) @@ -296,20 +295,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 NoVectorStoreClient(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 () => { @@ -321,11 +320,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) @@ -475,8 +474,8 @@ public partial class Information : MSGComponentBase protected override void DisposeResources() { - this.databaseRefreshCancellationTokenSource?.Cancel(); - this.databaseRefreshCancellationTokenSource?.Dispose(); + this.vectorStoreRefreshCancellationTokenSource?.Cancel(); + this.vectorStoreRefreshCancellationTokenSource?.Dispose(); base.DisposeResources(); } diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index e7b16e95..a88dbc8d 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -6078,6 +6078,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T103551060"] = "Die konfigurierte -- Browse AI Studio's source code on GitHub — we welcome your contributions. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1107156991"] = "Sehen Sie sich den Quellcode von AI Studio auf GitHub an – wir freuen uns über ihre Beiträge." +-- Vector store version +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1124039623"] = "Vektordatenbankversion" + +-- Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1126023000"] = "Qdrant Edge ist eine eingebettete Vektordatenbank und ein Vektoraehnlichkeitssuchmaschine. Wir nutzen sie, um lokal RAG – retrieval-augmented generation – innerhalb von AI Studio zu realisieren. Vielen Dank für die Anstrengungen und die großartige Arbeit, die in Qdrant investiert wurde und weiterhin investiert wird." + -- ID mismatch: the plugin ID differs from the enterprise configuration ID. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID-Konflikt: Die Plugin-ID stimmt nicht mit der ID der Unternehmenskonfiguration überein." @@ -6096,9 +6102,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Kopiert den Slot -- 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"] = "Diese Bibliothek wird verwendet, um PDF-Dateien zu lesen. Das ist zum Beispiel notwendig, um PDFs als Datenquelle für einen Chat zu nutzen." --- Database version -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1420062548"] = "Datenbankversion" - -- 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"] = "Diese Bibliothek wird verwendet, um die MudBlazor-Bibliothek zu erweitern. Sie stellt zusätzliche Komponenten bereit, die nicht Teil der MudBlazor-Bibliothek sind." @@ -6114,9 +6117,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Geheimnis für d -- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio wird mit Unternehmenskonfigurationen und Konfigurationsservern betrieben. Die Konfigurations-Plugins sind aktiv." --- Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG -— retrieval-augmented generation -— within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1619832053"] = "Qdrant ist eine Vektordatenbank und Suchmaschine für Vektoren. Wir nutzen Qdrant, um lokales RAG (Retrieval-Augmented Generation) innerhalb von AI Studio zu realisieren. Vielen Dank für den Einsatz und die großartige Arbeit, die in Qdrant gesteckt wurde und weiterhin gesteckt wird." - -- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "Wir verwenden Lua als Sprache für Plugins. Lua-CSharp ermöglicht die Kommunikation zwischen Lua-Skripten und AI Studio in beide Richtungen. Vielen Dank an Yusuke Nakada für diese großartige Bibliothek." @@ -6165,6 +6165,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgende -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage" +-- This library is used to create temporary folders in runtime tests and supporting filesystem operations. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "Diese Bibliothek wird verwendet, um temporäre Ordner bei Laufzeittests zu erstellen und Dateisystemoperationen zu unterstützen." + -- This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2173617769"] = "Diese Bibliothek wird verwendet, um den Dateityp einer Datei zu bestimmen. Das ist zum Beispiel notwendig, wenn wir eine Datei streamen möchten." @@ -6216,9 +6219,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build-Zeit" -- unknown UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2608177081"] = "unbekannt" --- This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2619858133"] = "Diese Bibliothek wird verwendet, um temporäre Ordner zu erstellen, in denen das Zertifikat und der private Schlüssel für die Kommunikation mit Qdrant gespeichert werden." - -- This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "Dieses Crate stellt Derive-Makros für Rust-Enums bereit, die wir verwenden, um Boilerplate zu reduzieren, wenn wir String-Konvertierungen und Metadaten für Laufzeittypen implementieren. Das ist hilfreich für die Kommunikation zwischen unseren Rust- und .NET-Systemen." @@ -6270,6 +6270,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2989678330"] = "Kopiert den Fing -- Changelog UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Änderungsprotokoll" +-- Vector store +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vektordatenbank" + -- External HTTPS custom root certificates are configured but not active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "Externe benutzerdefinierte Stammzertifikate sind konfiguriert, aber nicht aktiv." @@ -6378,9 +6381,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3986423270"] = "Pandoc-Installat -- Versions UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen" --- Database -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Datenbank" - -- Allowed hosts: none configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4058524336"] = "Zulässige Hosts: keine konfiguriert" @@ -7131,20 +7131,32 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = " -- Status UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" --- Storage size -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Speichergröße" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1093747001"] = "Grund" --- HTTP port -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP-Port" +-- Starting +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1233211769"] = "Starten" + +-- Unavailable +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T3662391977"] = "Nicht verfügbar" + +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T6222351"] = "Status" + +-- Storage size +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T1230141403"] = "Speichergröße" + +-- Number of vector stores +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T2785004838"] = "Anzahl der Vektordatenbanken" -- Reported version -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Gemeldete Version" +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T3556099842"] = "Gemeldete Version" --- gRPC port -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC-Port" +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T6222351"] = "Status" --- Number of collections -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Anzahl der Collections" +-- Qdrant Edge is not available. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge ist nicht verfügbar." -- 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"] = "Die zugehörigen Daten dürfen an keinen LLM-Anbieter gesendet werden. Das bedeutet, dass diese Datenquelle momentan nicht verwendet werden kann." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index a5e79f17..70eec49d 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -6078,6 +6078,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T103551060"] = "The configured ro -- 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" + +-- Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1126023000"] = "Qdrant Edge is an embedded vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant." + -- 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." @@ -6096,9 +6102,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1347508205"] = "Copies the confi -- 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." @@ -6114,9 +6117,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Encryption secre -- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active." --- Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG -— retrieval-augmented generation -— within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1619832053"] = "Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG -— retrieval-augmented generation -— within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant." - -- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T162898512"] = "We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library." @@ -6165,6 +6165,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" +-- This library is used to create temporary folders in runtime tests and supporting filesystem operations. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2160280545"] = "This library is used to create temporary folders in runtime tests and supporting filesystem operations." + -- This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2173617769"] = "This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file." @@ -6216,9 +6219,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time" -- unknown UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2608177081"] = "unknown" --- This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2619858133"] = "This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant." - -- This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2635482790"] = "This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems." @@ -6270,6 +6270,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2989678330"] = "Copies the root -- Changelog UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3017574265"] = "Changelog" +-- Vector store +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3046399223"] = "Vector store" -- External HTTPS custom root certificates are configured but not active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3021325354"] = "External HTTPS custom root certificates are configured but not active." @@ -6378,9 +6380,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" - -- Allowed hosts: none configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4058524336"] = "Allowed hosts: none configured" @@ -7131,20 +7130,32 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = " -- Status UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T6222351"] = "Status" --- Storage size -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1230141403"] = "Storage size" +-- Reason +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1093747001"] = "Reason" --- HTTP port -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T1717573768"] = "HTTP port" +-- Starting +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::NOVECTORSTORECLIENT::T1233211769"] = "Starting" + +-- 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::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T1230141403"] = "Storage size" + +-- Number of vector stores +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T2785004838"] = "Number of vector stores" -- Reported version -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T3556099842"] = "Reported version" +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T3556099842"] = "Reported version" --- gRPC port -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T757840040"] = "gRPC port" +-- Status +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T6222351"] = "Status" --- Number of collections -UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::QDRANT::QDRANTCLIENTIMPLEMENTATION::T842647336"] = "Number of collections" +-- Qdrant Edge is not available. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::VECTORSTORE::QDRANTEDGECLIENTIMPLEMENTATION::T744445696"] = "Qdrant Edge is not available." -- 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." diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs index 4296ec53..f22efa38 100644 --- a/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs @@ -1,6 +1,5 @@ -using AIStudio.Tools.Databases.Qdrant; -using AIStudio.Tools.Rust; using AIStudio.Tools.Services; +using AIStudio.Tools.Databases.VectorStore; namespace AIStudio.Tools.Databases; @@ -45,6 +44,18 @@ public sealed class DatabaseClientProvider(RustService rustService, ILoggerFacto } } + public async Task 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) @@ -80,90 +91,10 @@ public sealed class DatabaseClientProvider(RustService rustService, ILoggerFacto private async Task CreateClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken) => databaseRole switch { - DatabaseRole.VECTOR_STORE => await this.CreateQdrantClientAsync(cancellationToken), + DatabaseRole.VECTOR_STORE => await QdrantEdgeClientImplementation.CreateAsync(rustService, this.logger, this.databaseClientLogger, cancellationToken), _ => new NoDatabaseClient(databaseRole.ToString(), "The requested database role is not supported.") }; - private async Task 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 diff --git a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs deleted file mode 100644 index b3a09e68..00000000 --- a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs +++ /dev/null @@ -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 GetVersion() - { - var operation = await this.GrpcClient.HealthAsync(); - return $"v{operation.Version}"; - } - - public async Task CheckAvailabilityAsync() - { - await this.GrpcClient.HealthAsync(); - } - - private async Task 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(); -} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Databases/VectorStore/IVectorStoreClient.cs b/app/MindWork AI Studio/Tools/Databases/VectorStore/IVectorStoreClient.cs new file mode 100644 index 00000000..363cf902 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/VectorStore/IVectorStoreClient.cs @@ -0,0 +1,12 @@ +namespace AIStudio.Tools.Databases.VectorStore; + +public interface IVectorStoreClient +{ + Task EnsureVectorStoreExists(string storeName, int vectorSize, CancellationToken token); + + Task InsertEmbedding(string storeName, IReadOnlyList points, CancellationToken token); + + Task DeleteEmbeddingByFile(string storeName, string filePath, CancellationToken token); + + Task DeleteVectorStore(string storeName, CancellationToken token); +} diff --git a/app/MindWork AI Studio/Tools/Databases/VectorStore/NoVectorStoreClient.cs b/app/MindWork AI Studio/Tools/Databases/VectorStore/NoVectorStoreClient.cs new file mode 100644 index 00000000..75ed54da --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/VectorStore/NoVectorStoreClient.cs @@ -0,0 +1,43 @@ +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Tools.Databases.VectorStore; + +public sealed class NoVectorStoreClient(string name, string? unavailableReason, DatabaseClientStatus status = DatabaseClientStatus.UNAVAILABLE) : DatabaseClient(name, string.Empty), IVectorStoreClient +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoVectorStoreClient).Namespace, nameof(NoVectorStoreClient)); + + public override DatabaseClientStatus Status => status; + + public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo() + { + yield return (TB("Status"), status switch + { + DatabaseClientStatus.STARTING => TB("Starting"), + _ => 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 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."); + + public override void Dispose() + { + } +} diff --git a/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs new file mode 100644 index 00000000..6e606246 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/VectorStore/QdrantEdgeClientImplementation.cs @@ -0,0 +1,109 @@ +using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.Rust; +using AIStudio.Tools.Services; + +namespace AIStudio.Tools.Databases.VectorStore; + +public sealed class QdrantEdgeClientImplementation( + string name, + string path, + string version, + int storesCount, + RustService rustService) : DatabaseClient(name, path), IVectorStoreClient +{ + private const string DATABASE_NAME = "Qdrant Edge"; + private const string INFO_PATH = "/system/qdrant-edge/info"; + private const string ENSURE_PATH = "/system/qdrant-edge/ensure"; + private const string INSERT_PATH = "/system/qdrant-edge/insert"; + private const string DELETE_FILE_PATH = "/system/qdrant-edge/delete-file"; + private const string DELETE_STORE_PATH = "/system/qdrant-edge/delete-store"; + + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(QdrantEdgeClientImplementation).Namespace, nameof(QdrantEdgeClientImplementation)); + + public override string CacheKey => $"{this.Name}:{path}:{version}"; + + public static async Task CreateAsync( + RustService rustService, + ILogger logger, + ILogger databaseClientLogger, + CancellationToken cancellationToken) + { + var qdrantEdgeInfo = await rustService.GetDatabaseInfo( + DATABASE_NAME, + INFO_PATH, + QdrantEdgeInfo.Unavailable, + cancellationToken); + + if (qdrantEdgeInfo.Status is QdrantEdgeStatus.STARTING) + { + return CreateNoVectorStoreClient( + DATABASE_NAME, + $"{DATABASE_NAME} is starting. Details will appear shortly.", + DatabaseClientStatus.STARTING, + databaseClientLogger); + } + + if (!qdrantEdgeInfo.IsAvailable || qdrantEdgeInfo.Status is QdrantEdgeStatus.UNAVAILABLE) + { + var reason = qdrantEdgeInfo.UnavailableReason ?? "unknown"; + logger.LogWarning("{VectorStoreName} is not available. Starting without {VectorStoreName} vector store. Reason: '{Reason}'.", DATABASE_NAME, DATABASE_NAME, reason); + return CreateNoVectorStoreClient(DATABASE_NAME, qdrantEdgeInfo.UnavailableReason, DatabaseClientStatus.UNAVAILABLE, databaseClientLogger); + } + + if (qdrantEdgeInfo.Path == string.Empty) + return CreateNoVectorStoreClient(DATABASE_NAME, $"Failed to get the {DATABASE_NAME} path from Rust.", DatabaseClientStatus.UNAVAILABLE, databaseClientLogger); + + var name = string.IsNullOrWhiteSpace(qdrantEdgeInfo.Name) ? DATABASE_NAME : qdrantEdgeInfo.Name; + var client = new QdrantEdgeClientImplementation(name, qdrantEdgeInfo.Path, qdrantEdgeInfo.Version, qdrantEdgeInfo.StoresCount, rustService); + client.SetLogger(databaseClientLogger); + return client; + } + + public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo() + { + var currentInfo = await rustService.GetDatabaseInfo( + DATABASE_NAME, + INFO_PATH, + QdrantEdgeInfo.Unavailable); + var displayVersion = currentInfo.IsAvailable && !string.IsNullOrWhiteSpace(currentInfo.Version) ? currentInfo.Version : version; + var displayStoresCount = currentInfo.IsAvailable ? currentInfo.StoresCount : storesCount; + + if (!currentInfo.IsAvailable) + yield return (TB("Status"), currentInfo.UnavailableReason ?? TB("Qdrant Edge is not available.")); + + yield return (TB("Reported version"), displayVersion); + yield return (TB("Storage size"), $"{this.GetStorageSize()}"); + yield return (TB("Number of vector stores"), displayStoresCount.ToString()); + } + + public Task EnsureVectorStoreExists(string storeName, int vectorSize, CancellationToken token) => + rustService.ExecuteDatabaseOperation(DATABASE_NAME, ENSURE_PATH, new EnsureVectorStoreRequest(storeName, vectorSize), token); + + public Task InsertEmbedding(string storeName, IReadOnlyList points, CancellationToken token) => + rustService.ExecuteDatabaseOperation(DATABASE_NAME, INSERT_PATH, new InsertEmbeddingRequest(storeName, points), token); + + public Task DeleteEmbeddingByFile(string storeName, string filePath, CancellationToken token) => + rustService.ExecuteDatabaseOperation(DATABASE_NAME, DELETE_FILE_PATH, new DeleteEmbeddingByFileRequest(storeName, filePath), token); + + public Task DeleteVectorStore(string storeName, CancellationToken token) => + rustService.ExecuteDatabaseOperation(DATABASE_NAME, DELETE_STORE_PATH, new DeleteVectorStoreRequest(storeName), token); + + public override void Dispose() + { + } + + private static NoVectorStoreClient CreateNoVectorStoreClient(string name, string? unavailableReason, DatabaseClientStatus status, ILogger databaseClientLogger) + { + var client = new NoVectorStoreClient(name, unavailableReason, status); + client.SetLogger(databaseClientLogger); + return client; + } + + private sealed record EnsureVectorStoreRequest(string StoreName, int VectorSize); + + private sealed record InsertEmbeddingRequest(string StoreName, IReadOnlyList Points); + + private sealed record DeleteEmbeddingByFileRequest(string StoreName, string FilePath); + + private sealed record DeleteVectorStoreRequest(string StoreName); +} diff --git a/app/MindWork AI Studio/Tools/Databases/VectorStore/VectorStoragePoint.cs b/app/MindWork AI Studio/Tools/Databases/VectorStore/VectorStoragePoint.cs new file mode 100644 index 00000000..fc95ed38 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/VectorStore/VectorStoragePoint.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Tools.Databases.VectorStore; + +public sealed record VectorStoragePoint( + string PointId, + IReadOnlyList Vector, + string DataSourceId, + string DataSourceName, + string DataSourceType, + string FilePath, + string FileName, + string RelativePath, + int ChunkIndex, + string Text, + string Fingerprint, + DateTime LastWriteUtc, + DateTime EmbeddedAtUtc); diff --git a/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs b/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs deleted file mode 100644 index 5ef6064b..00000000 --- a/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -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/Metadata/MetaDataVectorStoreAttribute.cs b/app/MindWork AI Studio/Tools/Metadata/MetaDataVectorStoreAttribute.cs new file mode 100644 index 00000000..e3ba1b75 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Metadata/MetaDataVectorStoreAttribute.cs @@ -0,0 +1,6 @@ +namespace AIStudio.Tools.Metadata; + +public class MetaDataVectorStoreAttribute(string vectorStoreVersion) : Attribute +{ + public string VectorStoreVersion => vectorStoreVersion; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantEdgeInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantEdgeInfo.cs new file mode 100644 index 00000000..4e438f2f --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/QdrantEdgeInfo.cs @@ -0,0 +1,27 @@ +namespace AIStudio.Tools.Rust; + +/// +/// The response of the Qdrant Edge information request. +/// +public readonly record struct QdrantEdgeInfo +{ + public QdrantEdgeStatus Status { get; init; } + + public bool IsAvailable { get; init; } + + public string? UnavailableReason { get; init; } + + public string Name { get; init; } + + public string Version { get; init; } + + public string Path { get; init; } + + public int StoresCount { get; init; } + + public static QdrantEdgeInfo Unavailable(string reason) => new() + { + Status = QdrantEdgeStatus.UNAVAILABLE, + UnavailableReason = reason + }; +} diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs b/app/MindWork AI Studio/Tools/Rust/QdrantEdgeStatus.cs similarity index 72% rename from app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs rename to app/MindWork AI Studio/Tools/Rust/QdrantEdgeStatus.cs index 10d6246a..fb06dfc5 100644 --- a/app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs +++ b/app/MindWork AI Studio/Tools/Rust/QdrantEdgeStatus.cs @@ -1,8 +1,8 @@ namespace AIStudio.Tools.Rust; -public enum QdrantStatus +public enum QdrantEdgeStatus { STARTING, AVAILABLE, UNAVAILABLE, -} \ 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 deleted file mode 100644 index 30044596..00000000 --- a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace AIStudio.Tools.Rust; - -/// -/// The response of the Qdrant information request. -/// -public readonly record struct QdrantInfo -{ - public QdrantStatus Status { get; init; } - - public bool IsAvailable { get; init; } - - public string? UnavailableReason { get; init; } - - public string Path { get; init; } - - public int PortHttp { get; init; } - - public int PortGrpc { get; init; } - - public string Fingerprint { get; init; } - - public string ApiToken { 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 index 3efc8050..3f101d70 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs @@ -1,43 +1,53 @@ -using AIStudio.Tools.Rust; - namespace AIStudio.Tools.Services; public sealed partial class RustService { - public async Task GetQdrantInfo(CancellationToken cancellationToken = default) + public async Task GetDatabaseInfo( + string databaseName, + string infoPath, + Func unavailableFactory, + CancellationToken cancellationToken = default) { try { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(45)); - - return await this.http.GetFromJsonAsync("/system/qdrant/info", this.jsonRustSerializerOptions, cts.Token); + + var databaseInfo = await this.http.GetFromJsonAsync(infoPath, this.jsonRustSerializerOptions, cts.Token); + return databaseInfo ?? unavailableFactory("The database information response was empty."); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { if(this.logger is not null) - this.logger.LogWarning("Fetching Qdrant info from Rust service was cancelled by caller."); + this.logger.LogWarning("Fetching {DatabaseName} info from Rust service was cancelled by caller.", databaseName); else - Console.WriteLine("Fetching Qdrant info from Rust service was cancelled by caller."); - - return new QdrantInfo - { - Status = QdrantStatus.UNAVAILABLE, - UnavailableReason = "Operation cancelled by caller." - }; + Console.WriteLine($"Fetching {databaseName} info from Rust service was cancelled by caller."); + + return unavailableFactory("Operation cancelled by caller."); } catch (Exception e) { if(this.logger is not null) - this.logger.LogError(e, "Error while fetching Qdrant info from Rust service."); + this.logger.LogError(e, "Error while fetching {DatabaseName} info from Rust service.", databaseName); else - Console.WriteLine($"Error while fetching Qdrant info from Rust service: '{e}'."); - - return new QdrantInfo - { - Status = QdrantStatus.UNAVAILABLE, - UnavailableReason = e.Message - }; + Console.WriteLine($"Error while fetching {databaseName} info from Rust service: '{e}'."); + + return unavailableFactory(e.Message); } } -} \ No newline at end of file + + public async Task ExecuteDatabaseOperation(string databaseName, string path, TRequest request, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeSpan.FromMinutes(5)); + + using var response = await this.http.PostAsJsonAsync(path, request, this.jsonRustSerializerOptions, cts.Token); + response.EnsureSuccessStatusCode(); + + var operation = await response.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions, cts.Token); + if (operation is not { Success: true }) + throw new InvalidOperationException(operation?.Issue ?? $"The {databaseName} operation failed."); + } + + private sealed record DatabaseOperationResponse(bool Success, string Issue); +} diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 311fe569..65751edc 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -66,16 +66,6 @@ "MudBlazor": "8.11.0" } }, - "Qdrant.Client": { - "type": "Direct", - "requested": "[1.18.1, )", - "resolved": "1.18.1", - "contentHash": "eBwFLihGMvN02/jr/BNdcop2XmtA10y8VMOclVZ7K2H8yheAhl7jbkf7I8e4X3RYpT+cAxgcalP4xmOhgs4KJg==", - "dependencies": { - "Google.Protobuf": "3.31.0", - "Grpc.Net.Client": "2.71.0" - } - }, "ReverseMarkdown": { "type": "Direct", "requested": "[5.0.0, )", @@ -90,33 +80,6 @@ "resolved": "3.2.449", "contentHash": "uA9sYDy4VepL3xwzBTLcP2LyuVYMt0ZIT3gaSiXvGoX15Ob+rOP+hGydhevlSVd+rFo+Y+VQFEHDuWU8HBW+XA==" }, - "Google.Protobuf": { - "type": "Transitive", - "resolved": "3.31.0", - "contentHash": "OZXSf6igaJBeo+kAzMhYF0R5zp0nRgf4G0Uis/IsGKACc4RGP9bQPLpHLengIFuASl0lY92utMB8rRpTx4TaOg==" - }, - "Grpc.Core.Api": { - "type": "Transitive", - "resolved": "2.71.0", - "contentHash": "QquqUC37yxsDzd1QaDRsH2+uuznWPTS8CVE2Yzwl3CvU4geTNkolQXoVN812M2IwT6zpv3jsZRc9ExJFNFslTg==" - }, - "Grpc.Net.Client": { - "type": "Transitive", - "resolved": "2.71.0", - "contentHash": "U1vr20r5ngoT9nlb7wejF28EKN+taMhJsV9XtK9MkiepTZwnKxxiarriiMfCHuDAfPUm9XUjFMn/RIuJ4YY61w==", - "dependencies": { - "Grpc.Net.Common": "2.71.0", - "Microsoft.Extensions.Logging.Abstractions": "6.0.0" - } - }, - "Grpc.Net.Common": { - "type": "Transitive", - "resolved": "2.71.0", - "contentHash": "v0c8R97TwRYwNXlC8GyRXwYTCNufpDfUtj9la+wUrZFzVWkFJuNAltU+c0yI3zu0jl54k7en6u2WKgZgd57r2Q==", - "dependencies": { - "Grpc.Core.Api": "2.71.0" - } - }, "LuaCSharp.Annotations": { "type": "Transitive", "resolved": "0.5.5", diff --git a/documentation/Build.md b/documentation/Build.md index 8022cd7d..3301562e 100644 --- a/documentation/Build.md +++ b/documentation/Build.md @@ -50,13 +50,6 @@ You can now test your changes. To stop the application: - Press ``Ctrl+C`` in the terminal where the app is running. - Stop the process via your IDE’s run/debug controls. -> ⚠️ Important: Stopping the app via ``Ctrl+C`` or the IDE may not terminate the Qdrant sidecar process, especially on Windows. This can lead to startup failures when restarting the app. - -If you encounter issues with restarting Tauri, then manually kill the Qdrant process: -- **Linux/macOS:** Run pkill -f qdrant in your terminal. -- **Windows:** Open Task Manager → Find qdrant.exe → Right-click → “End task”. -- Restart your Tauri app. - ## Create a release In order to create a release: 1. To create a new release, you need to be a maintainer of the repository—see step 8. @@ -68,4 +61,4 @@ In order to create a release: 7. Your proposed changes will be reviewed and merged. 8. Once the PR is merged, a member of the maintainers team will create & push an appropriate git tag in the format `vX.Y.Z`. 9. The GitHub Workflow will then build the release and upload it to the [release page](https://github.com/MindWorkAI/AI-Studio/releases/latest). -10. Building the release including virus scanning takes some time. Please be patient. \ No newline at end of file +10. Building the release including virus scanning takes some time. Please be patient. diff --git a/metadata.txt b/metadata.txt index 0a0d5feb..7883dc5d 100644 --- a/metadata.txt +++ b/metadata.txt @@ -9,4 +9,4 @@ d05ff26e628, release osx-arm64 148.0.7763.0 -1.18.1 \ No newline at end of file +0.6.1 \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index aa27202c..f4586211 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -48,6 +48,7 @@ tempfile = "3.27.0" strum_macros = "0.28.0" sysinfo = "0.39.3" bytes = "1.11.1" +qdrant-edge = "0.6.1" [target.'cfg(target_os = "windows")'.dependencies] windows-registry = "0.6.1" diff --git a/runtime/capabilities/default.json b/runtime/capabilities/default.json index 86f14897..edd9c22f 100644 --- a/runtime/capabilities/default.json +++ b/runtime/capabilities/default.json @@ -22,11 +22,6 @@ "name": "mindworkAIStudioServer", "sidecar": true, "args": true - }, - { - "name": "qdrant", - "sidecar": true, - "args": true } ] } diff --git a/runtime/resources/databases/qdrant/config.yaml b/runtime/resources/databases/qdrant/config.yaml deleted file mode 100644 index 50f03e08..00000000 --- a/runtime/resources/databases/qdrant/config.yaml +++ /dev/null @@ -1,354 +0,0 @@ -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: false - - # Enable HTTPS for the REST and gRPC API - # TLS is enabled in AI Studio through environment variables when instantiating Qdrant as a sidecar. - # 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 dd54e205..f2d9c304 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -25,7 +25,7 @@ use crate::dotnet::{cleanup_dotnet_server, start_dotnet_server, stop_dotnet_serv 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, stop_qdrant_server}; +use crate::qdrant_edge_database::{start_qdrant_edge_database, stop_qdrant_edge_database}; #[cfg(debug_assertions)] use crate::dotnet::create_startup_env_file; @@ -148,7 +148,7 @@ pub fn start_tauri() { start_dotnet_server(app.handle().clone()); } - start_qdrant_server(app.handle().clone()); + start_qdrant_edge_database(app.handle().clone()); 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(); @@ -183,7 +183,7 @@ pub fn start_tauri() { RunEvent::ExitRequested { .. } => { warn!(Source = "Tauri"; "Run event: exit was requested."); - stop_qdrant_server(); + stop_qdrant_edge_database(); if is_prod() { warn!("Try to stop the .NET server as well..."); stop_dotnet_server(); @@ -537,7 +537,7 @@ pub async fn install_update(_token: APIToken) { if is_prod() { stop_dotnet_server(); - stop_qdrant_server(); + stop_qdrant_edge_database(); } else { warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server."); } @@ -1000,4 +1000,4 @@ mod tests { assert!(!is_tauri_asset_url(&url)); assert!(!is_local_http_url(&url)); } -} \ No newline at end of file +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index b36a1505..ac9f9250 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -13,7 +13,7 @@ pub mod file_data; pub mod metadata; pub mod pdfium; pub mod pandoc; -pub mod qdrant; +pub mod qdrant_edge_database; pub mod certificate_factory; pub mod runtime_api_token; pub mod stale_process_cleanup; diff --git a/runtime/src/main.rs b/runtime/src/main.rs index c03f26dc..a75f73eb 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -34,7 +34,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); + info!(".. Vector store: v{vector_store_version}", vector_store_version = metadata.vector_store_version); if is_dev() { warn!("Running in development mode."); diff --git a/runtime/src/metadata.rs b/runtime/src/metadata.rs index fa56dd68..df72640b 100644 --- a/runtime/src/metadata.rs +++ b/runtime/src/metadata.rs @@ -16,7 +16,7 @@ pub struct MetaData { pub app_commit_hash: String, pub architecture: String, pub pdfium_version: String, - pub qdrant_version: String, + pub vector_store_version: String, } impl MetaData { @@ -40,7 +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 vector_store_version = metadata_lines.next().unwrap(); let metadata = MetaData { architecture: architecture.to_string(), @@ -54,7 +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(), + vector_store_version: vector_store_version.to_string(), }; *META_DATA.lock().unwrap() = Some(metadata.clone()); diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs deleted file mode 100644 index 639dd7c7..00000000 --- a/runtime/src/qdrant.rs +++ /dev/null @@ -1,374 +0,0 @@ -use std::collections::HashMap; -use std::{fs}; -use std::error::Error; -use std::fs::File; -use std::io::Write; -use std::path::Path; -use std::sync::{Arc, Mutex, OnceLock}; -use std::time::Duration; -use log::{debug, error, info, warn}; -use once_cell::sync::Lazy; -use axum::Json; -use serde::Serialize; -use crate::api_token::{APIToken}; -use crate::environment::{is_dev, DATA_DIRECTORY}; -use crate::certificate_factory::generate_certificate; -use std::path::PathBuf; -use tauri::Manager; -use tauri::path::BaseDirectory; -use tempfile::{TempDir, Builder}; -use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; -use crate::sidecar_types::SidecarType; -use tokio::time; -use tauri_plugin_shell::process::{CommandChild, CommandEvent}; -use tauri_plugin_shell::ShellExt; - -// 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) -}); - -pub static CERTIFICATE_FINGERPRINT: OnceLock = OnceLock::new(); -static API_TOKEN: Lazy = Lazy::new(|| { - crate::api_token::generate_api_token() -}); - -static TMPDIR: Lazy>> = Lazy::new(|| Mutex::new(None)); -static QDRANT_STATUS: Lazy> = Lazy::new(|| Mutex::new(QdrantStatusInfo::default())); - -const PID_FILE_NAME: &str = "qdrant.pid"; -const SIDECAR_TYPE:SidecarType = SidecarType::Qdrant; -const STARTUP_TIMEOUT: Duration = Duration::from_secs(60); -const STARTUP_CHECK_INTERVAL: Duration = Duration::from_millis(250); - -#[derive(Clone, Copy, Default, Serialize, PartialEq, Eq)] -enum QdrantStatus { - #[default] - Starting, - Available, - Unavailable, -} - -#[derive(Default)] -struct QdrantStatusInfo { - status: QdrantStatus, - unavailable_reason: Option, -} - -fn qdrant_base_path() -> PathBuf { - let qdrant_directory = if is_dev() { "qdrant_test" } else { "qdrant" }; - Path::new(DATA_DIRECTORY.get().unwrap()) - .join("databases") - .join(qdrant_directory) -} - -#[derive(Serialize)] -pub struct ProvideQdrantInfo { - status: QdrantStatus, - path: String, - port_http: u16, - port_grpc: u16, - fingerprint: String, - api_token: String, - is_available: bool, - unavailable_reason: Option, -} - -pub async fn qdrant_port(_token: APIToken) -> Json { - let status = QDRANT_STATUS.lock().unwrap(); - let current_status = status.status; - let is_available = current_status == QdrantStatus::Available; - let unavailable_reason = status.unavailable_reason.clone(); - - Json(ProvideQdrantInfo { - status: current_status, - path: if is_available { - qdrant_base_path().to_string_lossy().to_string() - } else { - String::new() - }, - port_http: if is_available { *QDRANT_SERVER_PORT_HTTP } else { 0 }, - port_grpc: if is_available { *QDRANT_SERVER_PORT_GRPC } else { 0 }, - fingerprint: if is_available { - CERTIFICATE_FINGERPRINT.get().cloned().unwrap_or_default() - } else { - String::new() - }, - api_token: if is_available { - API_TOKEN.to_hex_text().to_string() - } else { - String::new() - }, - is_available, - unavailable_reason, - }) -} - -/// Starts the Qdrant server in a separate process. -pub fn start_qdrant_server(app_handle: tauri::AppHandle){ - set_qdrant_starting(); - tauri::async_runtime::spawn(async move { - cleanup_qdrant(); - start_qdrant_server_internal(app_handle); - }); -} - -fn start_qdrant_server_internal(app_handle: tauri::AppHandle){ - let path = qdrant_base_path(); - if !path.exists() && let Err(e) = fs::create_dir_all(&path){ - error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e); - set_qdrant_unavailable(format!("The Qdrant data directory could not be created: {e}")); - return; - } - - let (cert_path, key_path) = match create_temp_tls_files(&path) { - Ok(paths) => paths, - Err(e) => { - error!(Source="Qdrant"; "TLS files for Qdrant could not be created: {e}"); - set_qdrant_unavailable(format!("TLS files for Qdrant could not be created: {e}")); - return; - } - }; - - let storage_path = path.join("storage").to_string_lossy().to_string(); - let snapshot_path = path.join("snapshots").to_string_lossy().to_string(); - let init_path = path.join(".qdrant-initialized"); - let init_path_environment = init_path.to_string_lossy().to_string(); - - let qdrant_server_environment: HashMap = 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_environment), - (String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path), - (String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path), - (String::from("QDRANT__TLS__CERT"), cert_path.to_string_lossy().to_string()), - (String::from("QDRANT__TLS__KEY"), key_path.to_string_lossy().to_string()), - (String::from("QDRANT__SERVICE__ENABLE_TLS"), "true".to_string()), - (String::from("QDRANT__SERVICE__API_KEY"), API_TOKEN.to_hex_text().to_string()), - ]); - - let server_spawn_clone = QDRANT_SERVER.clone(); - let qdrant_relative_source_path = "resources/databases/qdrant/config.yaml"; - let qdrant_source_path = match app_handle.path().resolve(qdrant_relative_source_path, BaseDirectory::Resource) { - Ok(path) => path, - Err(_) => { - let reason = format!("The Qdrant config resource '{qdrant_relative_source_path}' could not be resolved."); - error!(Source = "Qdrant"; "{reason} Starting the app without Qdrant."); - set_qdrant_unavailable(reason); - return; - } - }; - - let qdrant_source_path_display = qdrant_source_path.to_string_lossy().to_string(); - tauri::async_runtime::spawn(async move { - let shell = app_handle.shell(); - - let sidecar = match shell.sidecar("qdrant") { - Ok(sidecar) => sidecar, - Err(e) => { - let reason = format!("Failed to create sidecar for Qdrant: {e}"); - error!(Source = "Qdrant"; "{reason}"); - set_qdrant_unavailable(reason); - return; - } - }; - - let (mut rx, child) = match sidecar - .args(["--config-path", qdrant_source_path_display.as_str()]) - .envs(qdrant_server_environment) - .spawn() - { - Ok(process) => process, - Err(e) => { - let reason = format!("Failed to spawn Qdrant server process with config path '{}': {e}", qdrant_source_path_display); - error!(Source = "Qdrant"; "{reason}"); - set_qdrant_unavailable(reason); - return; - } - }; - - let server_pid = child.pid(); - info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}."); - log_potential_stale_process(path.join(PID_FILE_NAME), server_pid, SIDECAR_TYPE); - - // Save the server process to stop it later: - *server_spawn_clone.lock().unwrap() = Some(child); - - let init_path_clone = init_path.clone(); - tauri::async_runtime::spawn(async move { - if wait_for_qdrant_startup(init_path_clone).await { - set_qdrant_available(); - info!(Source = "Qdrant"; "Qdrant is available."); - } else { - let reason = "Qdrant did not become available within the startup timeout.".to_string(); - error!(Source = "Qdrant"; "{reason}"); - set_qdrant_unavailable(reason); - } - }); - - // Log the output of the Qdrant server: - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Stdout(line) => { - let line_utf8 = String::from_utf8_lossy(&line).to_string(); - let line = line_utf8.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) => { - let line_utf8 = String::from_utf8_lossy(&line).to_string(); - error!(Source = "Qdrant Server (stderr)"; "{line_utf8}"); - }, - - _ => {} - } - } - - let is_available = QDRANT_STATUS.lock().unwrap().status == QdrantStatus::Available; - let unavailable_reason = if is_available { - "Qdrant server process stopped.".to_string() - } else { - "Qdrant server process stopped before it became available.".to_string() - }; - set_qdrant_unavailable(unavailable_reason); - }); -} - -/// 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(_) => { - set_qdrant_unavailable("Qdrant server was stopped.".to_string()); - warn!(Source = "Qdrant"; "Qdrant server process was stopped.") - }, - Err(e) => error!(Source = "Qdrant"; "Failed to stop Qdrant server process: {e}."), - } - } else { - warn!(Source = "Qdrant"; "Qdrant server process was not started or is already stopped."); - } - - drop_tmpdir(); - cleanup_qdrant(); -} - -async fn wait_for_qdrant_startup(init_path: PathBuf) -> bool { - let mut elapsed = Duration::ZERO; - while elapsed < STARTUP_TIMEOUT { - if init_path.exists() { - return true; - } - - time::sleep(STARTUP_CHECK_INTERVAL).await; - elapsed += STARTUP_CHECK_INTERVAL; - } - - false -} - -/// Create a temporary directory with TLS relevant files -pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box> { - let cert = generate_certificate(); - - let temp_dir = init_tmpdir_in(path); - let cert_path = temp_dir.join("cert.pem"); - let key_path = temp_dir.join("key.pem"); - - let mut cert_file = File::create(&cert_path)?; - cert_file.write_all(&cert.certificate)?; - - let mut key_file = File::create(&key_path)?; - key_file.write_all(&cert.private_key)?; - - CERTIFICATE_FINGERPRINT.set(cert.fingerprint).expect("Could not set the certificate fingerprint."); - - Ok((cert_path, key_path)) -} - -pub fn init_tmpdir_in>(path: P) -> PathBuf { - let mut guard = TMPDIR.lock().unwrap(); - let dir = guard.get_or_insert_with(|| { - Builder::new() - .prefix("cert-") - .tempdir_in(path) - .expect("failed to create tempdir") - }); - - dir.path().to_path_buf() -} - -pub fn drop_tmpdir() { - let mut guard = TMPDIR.lock().unwrap(); - *guard = None; - warn!(Source = "Qdrant"; "Temporary directory for TLS was dropped."); -} - -/// Remove old Pid files and kill the corresponding processes -pub fn cleanup_qdrant() { - let path = qdrant_base_path(); - let pid_path = path.join(PID_FILE_NAME); - if let Err(e) = kill_stale_process(pid_path, SIDECAR_TYPE) { - warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); - } - if let Err(e) = delete_old_certificates(path) { - warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); - } - -} - -fn set_qdrant_available() { - let mut status = QDRANT_STATUS.lock().unwrap(); - status.status = QdrantStatus::Available; - status.unavailable_reason = None; -} - -fn set_qdrant_starting() { - let mut status = QDRANT_STATUS.lock().unwrap(); - status.status = QdrantStatus::Starting; - status.unavailable_reason = None; -} - -fn set_qdrant_unavailable(reason: String) { - let mut status = QDRANT_STATUS.lock().unwrap(); - status.status = QdrantStatus::Unavailable; - status.unavailable_reason = Some(reason); -} - -pub fn delete_old_certificates(path: PathBuf) -> Result<(), Box> { - if !path.exists() { - return Ok(()); - } - - for entry in fs::read_dir(path)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - let file_name = entry.file_name(); - let folder_name = file_name.to_string_lossy(); - - if folder_name.starts_with("cert-") { - fs::remove_dir_all(&path)?; - warn!(Source="Qdrant"; "Removed old certificates in: {}", path.display()); - } - } - } - Ok(()) -} \ No newline at end of file diff --git a/runtime/src/qdrant_edge_database.rs b/runtime/src/qdrant_edge_database.rs new file mode 100644 index 00000000..88f9bf9a --- /dev/null +++ b/runtime/src/qdrant_edge_database.rs @@ -0,0 +1,587 @@ +use std::collections::HashMap; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; + +use axum::Json; +use log::{error, info, warn}; +use once_cell::sync::Lazy; +use qdrant_edge::external::serde_json::json; +use qdrant_edge::external::uuid::Uuid; +use qdrant_edge::{ + Condition, Distance, EdgeConfig, EdgeOptimizersConfig, EdgeShard, EdgeVectorParams, + FieldCondition, Filter, HnswIndexConfig, Match, MatchValue, PointId, PointInsertOperations, + PointOperations, PointStruct, UpdateOperation, ValueVariants, Vectors, +}; +use serde::{Deserialize, Serialize}; +use tauri::Manager; + +use crate::api_token::APIToken; +use crate::environment::DATA_DIRECTORY; +use crate::metadata::META_DATA; + +const VECTOR_NAME: &str = "embedding"; +const HNSW_M: usize = 16; +const HNSW_EF_CONSTRUCT: usize = 100; +const HNSW_FULL_SCAN_THRESHOLD_KB: usize = 10_000; +const HNSW_MAX_INDEXING_THREADS: usize = 0; +const VECTOR_INDEXING_THRESHOLD_KB: usize = 10_000; + +type QdrantEdgeResult = Result>; + +static QDRANT_EDGE_DATABASE: Lazy>> = + Lazy::new(|| Mutex::new(None)); + +static QDRANT_EDGE_STATUS: Lazy> = + Lazy::new(|| Mutex::new(QdrantEdgeStatusInfo::default())); + +#[derive(Default)] +struct QdrantEdgeStatusInfo { + status: QdrantEdgeStatus, + unavailable_reason: Option, +} + +#[derive(Clone, Copy, Default, Serialize, PartialEq, Eq)] +pub enum QdrantEdgeStatus { + #[default] + Starting, + Available, + Unavailable, +} + +#[derive(Serialize)] +pub struct QdrantEdgeServiceInfo { + pub status: QdrantEdgeStatus, + pub name: String, + pub version: String, + pub path: String, + pub stores_count: usize, + pub is_available: bool, + pub unavailable_reason: Option, +} + +#[derive(Clone, Deserialize)] +pub struct QdrantEdgeStoragePoint { + pub point_id: String, + pub vector: Vec, + pub data_source_id: String, + pub data_source_name: String, + pub data_source_type: String, + pub file_path: String, + pub file_name: String, + pub relative_path: String, + pub chunk_index: i32, + pub text: String, + pub fingerprint: String, + pub last_write_utc: String, + pub embedded_at_utc: String, +} + +#[derive(Deserialize)] +pub struct EnsureQdrantEdgeStoreRequest { + pub store_name: String, + pub vector_size: usize, +} + +#[derive(Deserialize)] +pub struct InsertQdrantEdgeEmbeddingRequest { + pub store_name: String, + pub points: Vec, +} + +#[derive(Deserialize)] +pub struct DeleteQdrantEdgeEmbeddingByFileRequest { + pub store_name: String, + pub file_path: String, +} + +#[derive(Deserialize)] +pub struct DeleteQdrantEdgeStoreRequest { + pub store_name: String, +} + +#[derive(Serialize)] +pub struct QdrantEdgeOperationResponse { + pub success: bool, + pub issue: String, +} + +#[derive(Clone, Serialize)] +pub struct QdrantEdgeInfo { + pub name: String, + pub version: String, + pub path: String, + pub stores_count: usize, +} + +pub struct QdrantEdgeDatabase { + base_path: PathBuf, + shards: HashMap, +} + +impl QdrantEdgeDatabase { + pub fn new(base_path: PathBuf) -> Self { + Self { + base_path, + shards: HashMap::new(), + } + } + + fn store_path(&self, store_name: &str) -> QdrantEdgeResult { + validate_store_name(store_name)?; + Ok(self.base_path.join("stores").join(store_name)) + } + + // To ensure a shard exists and that you can insert a vector + fn get_or_create_store(&mut self, store_name: &str, vector_size: usize) -> QdrantEdgeResult<&EdgeShard> { + if self.shards.contains_key(store_name) { + return Ok(self.shards.get(store_name).unwrap()); + } + + let path = self.store_path(store_name)?; + let shard = if has_existing_store(&path) { + EdgeShard::load(&path, None)? + } else { + fs::create_dir_all(&path)?; + EdgeShard::new(&path, edge_config(vector_size))? + }; + + self.shards.insert(store_name.to_string(), shard); + Ok(self.shards.get(store_name).unwrap()) + } + + // To check whether a shard exists so you can delete a file from it + fn get_existing_store(&mut self, store_name: &str) -> QdrantEdgeResult> { + if self.shards.contains_key(store_name) { + return Ok(self.shards.get(store_name)); + } + + let path = self.store_path(store_name)?; + if !has_existing_store(&path) { + return Ok(None); + } + + let shard = EdgeShard::load(&path, None)?; + self.shards.insert(store_name.to_string(), shard); + Ok(self.shards.get(store_name)) + } + + fn info(&self) -> QdrantEdgeResult { + let stores_path = self.base_path.join("stores"); + let stores_count = if stores_path.exists() { + fs::read_dir(stores_path)? + .filter_map(Result::ok) + .filter(|entry| entry.path().is_dir()) + .count() + } else { + 0 + }; + + Ok(QdrantEdgeInfo { + name: "Qdrant Edge".to_string(), + version: vector_store_version()?, + path: self.base_path.to_string_lossy().to_string(), + stores_count, + }) + } + + fn ensure_store_exists(&mut self, store_name: &str, vector_size: usize) -> QdrantEdgeResult<()> { + validate_vector_size(vector_size)?; + self.get_or_create_store(store_name, vector_size)?; + Ok(()) + } + + fn insert_embedding(&mut self, store_name: &str, points: Vec) -> QdrantEdgeResult<()> { + let Some(first_point) = points.first() else { + return Ok(()); + }; + + let vector_size = first_point.vector.len(); + validate_vector_size(vector_size)?; + if points.iter().any(|point| point.vector.len() != vector_size) { + return Err("All vectors in one insert request must have the same size.".into()); + } + + let shard = self.get_or_create_store(store_name, vector_size)?; + let points = points + .into_iter() + .map(to_qdrant_edge_point) + .collect::>(); + + shard.update(UpdateOperation::PointOperation( + PointOperations::UpsertPoints(PointInsertOperations::PointsList(points)), + ))?; + shard.flush(); + Ok(()) + } + + fn delete_embedding_by_file(&mut self, store_name: &str, file_path: &str) -> QdrantEdgeResult<()> { + let Some(shard) = self.get_existing_store(store_name)? else { + return Ok(()); + }; + + shard.update(UpdateOperation::PointOperation( + PointOperations::DeletePointsByFilter(match_keyword_filter("file_path", file_path)?), + ))?; + shard.flush(); + Ok(()) + } + + fn delete_store(&mut self, store_name: &str) -> QdrantEdgeResult<()> { + self.shards.remove(store_name); + + let path = self.store_path(store_name)?; + if path.exists() { + fs::remove_dir_all(path)?; + } + + Ok(()) + } + + fn base_path(&self) -> PathBuf { + self.base_path.clone() + } +} + +fn qdrant_edge_base_path() -> QdrantEdgeResult { + let data_directory = DATA_DIRECTORY + .get() + .ok_or("The data directory has not been initialized.")?; + + Ok(Path::new(data_directory) + .join("databases") + .join("vector_database")) +} + +pub async fn qdrant_edge_info(_token: APIToken) -> Json { + let status = QDRANT_EDGE_STATUS.lock().unwrap(); + let current_status = status.status; + let unavailable_reason = status.unavailable_reason.clone(); + drop(status); + + let database_guard = QDRANT_EDGE_DATABASE.lock().unwrap(); + let database_info = database_guard + .as_ref() + .and_then(|database| database.info().ok()); + + let is_available = current_status == QdrantEdgeStatus::Available && database_info.is_some(); + Json(QdrantEdgeServiceInfo { + status: current_status, + name: database_info.as_ref().map(|info| info.name.clone()).unwrap_or_default(), + version: database_info.as_ref().map(|info| info.version.clone()).unwrap_or_default(), + path: database_info.as_ref().map(|info| info.path.clone()).unwrap_or_default(), + stores_count: database_info.as_ref().map(|info| info.stores_count).unwrap_or_default(), + is_available, + unavailable_reason, + }) +} + +pub async fn ensure_qdrant_edge_store(_token: APIToken, Json(request): Json) -> Json { + execute_qdrant_edge_operation(|database| { + database.ensure_store_exists(&request.store_name, request.vector_size) + }) +} + +pub async fn insert_qdrant_edge_embedding(_token: APIToken, Json(request): Json) -> Json { + execute_qdrant_edge_operation(|database| { + database.insert_embedding(&request.store_name, request.points) + }) +} + +pub async fn delete_qdrant_edge_embedding_by_file(_token: APIToken, Json(request): Json) -> Json { + execute_qdrant_edge_operation(|database| { + database.delete_embedding_by_file(&request.store_name, &request.file_path) + }) +} + +pub async fn delete_qdrant_edge_store(_token: APIToken, Json(request): Json) -> Json { + execute_qdrant_edge_operation(|database| { + database.delete_store(&request.store_name) + }) +} + +pub fn start_qdrant_edge_database(app_handle: tauri::AppHandle) { + set_qdrant_edge_starting(); + remove_obsolete_qdrant_sidecar_files(&app_handle); + + let path = match qdrant_edge_base_path() { + Ok(path) => path, + Err(e) => { + let reason = format!("Qdrant Edge cannot be started: {e}"); + error!(Source = "Qdrant Edge"; "{reason}"); + set_qdrant_edge_unavailable(reason); + return; + }, + }; + + match fs::create_dir_all(&path) { + Ok(_) => { + let database = QdrantEdgeDatabase::new(path.clone()); + *QDRANT_EDGE_DATABASE.lock().unwrap() = Some(database); + set_qdrant_edge_available(); + info!(Source = "Qdrant Edge"; "Qdrant Edge is available at '{}'.", path.display()); + }, + Err(e) => { + let reason = format!("The Qdrant Edge data directory could not be created: {e}"); + error!(Source = "Qdrant Edge"; "{reason}"); + set_qdrant_edge_unavailable(reason); + }, + } +} + +pub fn stop_qdrant_edge_database() { + if let Some(database) = QDRANT_EDGE_DATABASE.lock().unwrap().take() { + info!(Source = "Qdrant Edge"; "Stopping Qdrant Edge at '{}'.", database.base_path().display()); + drop(database); + } + + set_qdrant_edge_unavailable("Qdrant Edge was stopped.".to_string()); +} + +fn execute_qdrant_edge_operation(operation: F) -> Json +where + F: FnOnce(&mut QdrantEdgeDatabase) -> QdrantEdgeResult<()>, +{ + let mut database_guard = QDRANT_EDGE_DATABASE.lock().unwrap(); + let Some(database) = database_guard.as_mut() else { + return Json(QdrantEdgeOperationResponse { + success: false, + issue: "Qdrant Edge is not available.".to_string(), + }); + }; + + match operation(database) { + Ok(_) => Json(QdrantEdgeOperationResponse { + success: true, + issue: String::new(), + }), + Err(e) => { + let issue = e.to_string(); + error!(Source = "Qdrant Edge"; "Qdrant Edge operation failed: {issue}"); + Json(QdrantEdgeOperationResponse { + success: false, + issue, + }) + }, + } +} + +fn set_qdrant_edge_available() { + let mut status = QDRANT_EDGE_STATUS.lock().unwrap(); + status.status = QdrantEdgeStatus::Available; + status.unavailable_reason = None; +} + +fn set_qdrant_edge_starting() { + let mut status = QDRANT_EDGE_STATUS.lock().unwrap(); + status.status = QdrantEdgeStatus::Starting; + status.unavailable_reason = None; +} + +fn set_qdrant_edge_unavailable(reason: String) { + let mut status = QDRANT_EDGE_STATUS.lock().unwrap(); + status.status = QdrantEdgeStatus::Unavailable; + status.unavailable_reason = Some(reason); +} + +fn remove_obsolete_qdrant_sidecar_files(app_handle: &tauri::AppHandle) { + let mut paths = Vec::new(); + + if let Some(data_directory) = DATA_DIRECTORY.get() { + let databases_directory = Path::new(data_directory).join("databases"); + paths.push(databases_directory.join("qdrant")); + paths.push(databases_directory.join("qdrant_test")); + } + + if let Ok(resource_dir) = app_handle.path().resource_dir() { + paths.push(resource_dir.join("target").join("databases").join("qdrant")); + paths.push(resource_dir.join("resources").join("databases").join("qdrant")); + } + + cfg_if::cfg_if! { + if #[cfg(any(target_os = "windows", target_os = "macos"))]{ + if let Ok(current_exe) = std::env::current_exe() && let Some(exe_dir) = current_exe.parent() { + if (exe_dir.to_string_lossy().contains("MindWork AI Studio")) { + paths.push(exe_dir.join("target").join("databases").join("qdrant")); + paths.push(exe_dir.join("qdrant.exe")); + paths.push(exe_dir.join("qdrant")); + } + } + } + } + + for path in paths { + remove_obsolete_qdrant_path(&path); + } +} + +fn remove_obsolete_qdrant_path(path: &Path) { + if !path.exists() { + info!(Source = "Qdrant Edge"; "Obsolete file or directory '{}' was not found.", path.display()); + return; + } + + let result = if path.is_dir() { + fs::remove_dir_all(path) + } else { + fs::remove_file(path) + }; + + match result { + Ok(_) => warn!(Source = "Qdrant Edge"; "Removed obsolete Qdrant sidecar file or directory '{}'.", path.display()), + Err(e) => warn!(Source = "Qdrant Edge"; "Could not remove obsolete Qdrant sidecar file or directory '{}': {e}", path.display()), + } +} + +fn edge_config(vector_size: usize) -> EdgeConfig { + EdgeConfig { + on_disk_payload: true, + vectors: HashMap::from([( + VECTOR_NAME.to_string(), + EdgeVectorParams { + size: vector_size, + distance: Distance::Cosine, + on_disk: Some(true), + quantization_config: None, + multivector_config: None, + datatype: None, + hnsw_config: Some(hnsw_config()), + }, + )]), + sparse_vectors: HashMap::new(), + hnsw_config: hnsw_config(), + quantization_config: None, + optimizers: edge_optimizers_config(), + } +} + +fn hnsw_config() -> HnswIndexConfig { + HnswIndexConfig { + m: HNSW_M, + ef_construct: HNSW_EF_CONSTRUCT, + full_scan_threshold: HNSW_FULL_SCAN_THRESHOLD_KB, + max_indexing_threads: HNSW_MAX_INDEXING_THREADS, + on_disk: Some(true), + payload_m: None, + inline_storage: None, + } +} + +fn edge_optimizers_config() -> EdgeOptimizersConfig { + EdgeOptimizersConfig { + indexing_threshold: Some(VECTOR_INDEXING_THRESHOLD_KB), + prevent_unoptimized: Some(false), + ..Default::default() + } +} + +fn has_existing_store(path: &Path) -> bool { + path.join("edge_config.json").exists() || path.join("segments").exists() +} + +fn validate_vector_size(vector_size: usize) -> QdrantEdgeResult<()> { + if vector_size == 0 { + return Err("Vector size must be greater than zero.".into()); + } + + Ok(()) +} + +fn vector_store_version() -> QdrantEdgeResult { + let metadata = META_DATA + .lock() + .map_err(|_| "Metadata lock was poisoned.")?; + let Some(metadata) = metadata.as_ref() else { + return Err("Metadata was not initialized.".into()); + }; + + Ok(metadata.vector_store_version.clone()) +} + +fn to_qdrant_edge_point(point: QdrantEdgeStoragePoint) -> qdrant_edge::PointStructPersisted { + PointStruct::new( + to_point_id(&point.point_id), + Vectors::new_named([(VECTOR_NAME, point.vector)]), + json!({ + "data_source_id": point.data_source_id, + "data_source_name": point.data_source_name, + "data_source_type": point.data_source_type, + "file_path": point.file_path, + "file_name": point.file_name, + "relative_path": point.relative_path, + "chunk_index": point.chunk_index, + "text": point.text, + "fingerprint": point.fingerprint, + "last_write_utc": point.last_write_utc, + "embedded_at_utc": point.embedded_at_utc, + }), + ) + .into() +} + +fn to_point_id(point_id: &str) -> PointId { + Uuid::parse_str(point_id) + .map(PointId::Uuid) + .unwrap_or_else(|_| PointId::NumId(stable_u64(point_id))) +} + +fn stable_u64(value: &str) -> u64 { + let mut hash = 0xcbf29ce484222325_u64; + for byte in value.as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + + hash +} + +fn match_keyword_filter(field_name: &str, value: &str) -> QdrantEdgeResult { + Ok(Filter { + should: None, + min_should: None, + must: Some(vec![Condition::Field(FieldCondition::new_match( + field_name + .try_into() + .map_err(|_| format!("Invalid payload field name '{field_name}'."))?, + Match::Value(MatchValue { + value: ValueVariants::String(value.to_string()), + }), + ))]), + must_not: None, + }) +} + +fn validate_store_name(store_name: &str) -> QdrantEdgeResult<()> { + if store_name.is_empty() { + return Err("Vector store name cannot be empty.".into()); + } + + if matches!(store_name, "." | "..") { + return Err(format!("Vector store name '{store_name}' is not supported.").into()); + } + + if store_name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.') + { + return Ok(()); + } + + Err(format!("Vector store name '{store_name}' contains unsupported characters.").into()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_store_name_allows_safe_store_names() { + assert!(validate_store_name("rag_1234-abcd.ef").is_ok()); + } + + #[test] + fn validate_store_name_rejects_path_traversal_names() { + assert!(validate_store_name(".").is_err()); + assert!(validate_store_name("..").is_err()); + } +} diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 590acab2..087c0ffd 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -32,7 +32,11 @@ pub fn start_runtime_api() { let app = Router::new() .route("/system/dotnet/port", get(crate::dotnet::dotnet_port)) .route("/system/dotnet/ready", get(crate::dotnet::dotnet_ready)) - .route("/system/qdrant/info", get(crate::qdrant::qdrant_port)) + .route("/system/qdrant-edge/info", get(crate::qdrant_edge_database::qdrant_edge_info)) + .route("/system/qdrant-edge/ensure", post(crate::qdrant_edge_database::ensure_qdrant_edge_store)) + .route("/system/qdrant-edge/insert", post(crate::qdrant_edge_database::insert_qdrant_edge_embedding)) + .route("/system/qdrant-edge/delete-file", post(crate::qdrant_edge_database::delete_qdrant_edge_embedding_by_file)) + .route("/system/qdrant-edge/delete-store", post(crate::qdrant_edge_database::delete_qdrant_edge_store)) .route("/clipboard/set", post(crate::clipboard::set_clipboard)) .route("/events", get(crate::app_window::get_event_stream)) .route("/updates/check", get(crate::app_window::check_for_update)) @@ -81,4 +85,4 @@ fn install_rustls_crypto_provider() { RUSTLS_CRYPTO_PROVIDER_INIT.call_once(|| { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); }); -} \ No newline at end of file +} diff --git a/runtime/src/sidecar_types.rs b/runtime/src/sidecar_types.rs index 7e5bfde0..973aa603 100644 --- a/runtime/src/sidecar_types.rs +++ b/runtime/src/sidecar_types.rs @@ -2,14 +2,12 @@ pub enum SidecarType { Dotnet, - Qdrant, } impl fmt::Display for SidecarType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { SidecarType::Dotnet => write!(f, ".Net"), - SidecarType::Qdrant => write!(f, "Qdrant"), } } } \ No newline at end of file diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 1e1a96e9..e29bb1a4 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -24,11 +24,9 @@ "icons/icon.ico" ], "externalBin": [ - "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", - "target/databases/qdrant/qdrant" + "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer" ], "resources": [ - "resources/databases/qdrant/config.yaml", "resources/libraries/*" ], "macOS": {