From dec2c27997fea7c7d4630e1dd9aedce98d62b01a Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Wed, 3 Dec 2025 17:21:25 +0100 Subject: [PATCH 1/2] Added functionality to download Qdrant and execute it as a background sidecar. --- .github/workflows/build-and-release.yml | 125 +++++++ .gitignore | 7 + app/Build/Commands/Database.cs | 3 + app/Build/Commands/Qdrant.cs | 119 ++++++ app/Build/Commands/UpdateMetadataCommands.cs | 13 + .../Assistants/I18N/allTexts.lua | 6 + .../MindWork AI Studio.csproj | 4 + app/MindWork AI Studio/Pages/About.razor | 2 + app/MindWork AI Studio/Pages/About.razor.cs | 3 + .../Metadata/MetaDataDatabasesAttribute.cs | 6 + metadata.txt | 3 +- .../resources/databases/qdrant/config.yaml | 353 ++++++++++++++++++ runtime/src/app_window.rs | 4 + runtime/src/lib.rs | 3 +- runtime/src/main.rs | 1 + runtime/src/metadata.rs | 3 + runtime/src/qdrant.rs | 122 ++++++ runtime/tauri.conf.json | 8 +- 18 files changed, 782 insertions(+), 3 deletions(-) create mode 100644 app/Build/Commands/Database.cs create mode 100644 app/Build/Commands/Qdrant.cs create mode 100644 app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs create mode 100644 runtime/resources/databases/qdrant/config.yaml create mode 100644 runtime/src/qdrant.rs diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 8d1d8de4a..0d47e4d23 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -173,6 +173,9 @@ jobs: pdfium_version=$(sed -n '11p' metadata.txt) pdfium_version=$(echo $pdfium_version | cut -d'.' -f3) + # Next line is the Qdrant version: + qdrant_version=$(sed -n '12p' metadata.txt) + # Write the metadata to the environment: echo "APP_VERSION=${app_version}" >> $GITHUB_ENV echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV @@ -185,6 +188,7 @@ jobs: echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV echo "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $GITHUB_ENV echo "PDFIUM_VERSION=${pdfium_version}" >> $GITHUB_ENV + echo "QDRANT_VERSION=${qdrant_version}" >> $GITHUB_ENV # Log the metadata: echo "App version: '${formatted_app_version}'" @@ -197,6 +201,7 @@ jobs: echo "Tauri version: '${tauri_version}'" echo "Architecture: '${{ matrix.dotnet_runtime }}'" echo "PDFium version: '${pdfium_version}'" + echo "Qdrant version: '${qdrant_version}'" - name: Read and format metadata (Windows) if: matrix.platform == 'windows-latest' @@ -241,6 +246,9 @@ jobs: $pdfium_version = $metadata[10] $pdfium_version = $pdfium_version.Split('.')[2] + # Next line is the necessary Qdrant version: + $qdrant_version = $metadata[12] + # Write the metadata to the environment: Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV Write-Output "FORMATTED_APP_VERSION=${formatted_app_version}" >> $env:GITHUB_ENV @@ -252,6 +260,7 @@ jobs: Write-Output "MUD_BLAZOR_VERSION=${mud_blazor_version}" >> $env:GITHUB_ENV Write-Output "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $env:GITHUB_ENV Write-Output "PDFIUM_VERSION=${pdfium_version}" >> $env:GITHUB_ENV + Write-Output "QDRANT_VERSION=${qdrant_version}" >> $env:GITHUB_ENV # Log the metadata: Write-Output "App version: '${formatted_app_version}'" @@ -264,6 +273,7 @@ jobs: Write-Output "Tauri version: '${tauri_version}'" Write-Output "Architecture: '${{ matrix.dotnet_runtime }}'" Write-Output "PDFium version: '${pdfium_version}'" + Write-Output "Qdrant version: '${qdrant_version}'" - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -385,6 +395,121 @@ jobs: Write-Host "Cleaning up ..." Remove-Item $ARCHIVE -Force -ErrorAction SilentlyContinue + # Try to remove the temporary directory, but ignore errors if files are still in use + try { + Remove-Item $TMP -Recurse -Force -ErrorAction Stop + Write-Host "Successfully cleaned up temporary directory: $TMP" + } catch { + Write-Warning "Could not fully clean up temporary directory: $TMP. This is usually harmless as Windows will clean it up later. Error: $($_.Exception.Message)" + } + - name: Deploy Qdrant (Unix) + if: matrix.platform != 'windows-latest' + env: + QDRANT_VERSION: ${{ env.QDRANT_VERSION }} + DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }} + run: | + set -e + + # Target directory: + TDB_DIR="runtime/resources/databases/qdrant" + mkdir -p "$TDB_DIR" + + case "${DOTNET_RUNTIME}" in + linux-x64) + QDRANT_FILE="x86_64-unknown-linux-gnu.tar.gz" + DB_SOURCE="qdrant" + DB_TARGET="qdrant" + ;; + linux-arm64) + QDRANT_FILE="aarch64-unknown-linux-musl.tar.gz" + DB_SOURCE="qdrant" + DB_TARGET="qdrant" + ;; + osx-x64) + QDRANT_FILE="x86_64-apple-darwin.tar.gz" + DB_SOURCE="qdrant" + DB_TARGET="qdrant" + ;; + osx-arm64) + QDRANT_FILE="aarch64-apple-darwin.tar.gz" + DB_SOURCE="qdrant" + DB_TARGET="qdrant" + ;; + *) + echo "Unknown platform: ${DOTNET_RUNTIME}" + exit 1 + ;; + esac + + QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSON}/qdrant-{QDRANT_FILE}" + + echo "Download Qdrant $QDRANT_URL ..." + TMP=$(mktemp -d) + ARCHIVE="${TMP}/qdrant.tgz" + + curl -fsSL -o "$ARCHIVE" "$QDRANT_URL" + + echo "Extracting Qdrant ..." + tar xzf "$ARCHIVE" -C "$TMP" + SRC="${TMP}/${DB_SOURCE}" + + if [ ! -f "$SRC" ]; then + echo "Was not able to find Qdrant source: $SRC" + exit 1 + fi + + echo "Copy Qdrant from ${DB_TARGET} to ${TDB_DIR}/" + cp -f "$SRC" "$TDB_DIR/$DB_TARGET" + + echo "Cleaning up ..." + rm -fr "$TMP" + + - name: Install Qdrant (Windows) + if: matrix.platform == 'windows-latest' + env: + QDRANT_VERSION: ${{ env.QDRANT_VERSION }} + DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }} + run: | + $TDB_DIR = "runtime\resources\databases\qdrant" + New-Item -ItemType Directory -Force -Path $TDB_DIR | Out-Null + + switch ($env:DOTNET_RUNTIME) { + "win-x64" { + $QDRANT_FILE = "x86_64-pc-windows-msvc.zip" + $DB_SOURCE = "qdrant.exe" + $DB_TARGET = "qdrant.exe" + } + default { + Write-Error "Unknown platform: $($env:DOTNET_RUNTIME)" + exit 1 + } + } + + QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSON}/qdrant-{QDRANT_FILE}" + Write-Host "Download $QDRANT_URL ..." + + # Create a unique temporary directory (not just a file) + $TMP = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) + New-Item -ItemType Directory -Path $TMP -Force | Out-Null + $ARCHIVE = Join-Path $TMP "qdrant.tgz" + + Invoke-WebRequest -Uri $QDRANT_URL -OutFile $ARCHIVE + + Write-Host "Extracting Qdrant ..." + tar -xzf $ARCHIVE -C $TMP + + $SRC = Join-Path $TMP $DB_SOURCE + if (!(Test-Path $SRC)) { + Write-Error "Cannot find Qdrant source: $SRC" + exit 1 + } + + $DEST = Join-Path $TDB_DIR $DB_TARGET + Copy-Item -Path $SRC -Destination $DEST -Force + + Write-Host "Cleaning up ..." + Remove-Item $ARCHIVE -Force -ErrorAction SilentlyContinue + # Try to remove the temporary directory, but ignore errors if files are still in use try { Remove-Item $TMP -Recurse -Force -ErrorAction Stop diff --git a/.gitignore b/.gitignore index 81a01256c..cefdb8456 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,13 @@ libpdfium.dylib libpdfium.so libpdfium.dll +# Ignore qdrant database: +qdrant-aarch64-apple-darwin +qdrant-x86_64-apple-darwin +qdrant-aarch64-unknown-linux-gnu +qdrant-x86_64-unknown-linux-gnu +qdrant-x86_64-pc-windows-msvc.exe + # User-specific files *.rsuser *.suo diff --git a/app/Build/Commands/Database.cs b/app/Build/Commands/Database.cs new file mode 100644 index 000000000..dcd78391d --- /dev/null +++ b/app/Build/Commands/Database.cs @@ -0,0 +1,3 @@ +namespace Build.Commands; + +public record Database(string Path, string Filename); \ No newline at end of file diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs new file mode 100644 index 000000000..cc0bb1e4e --- /dev/null +++ b/app/Build/Commands/Qdrant.cs @@ -0,0 +1,119 @@ +using System.Diagnostics.Eventing.Reader; +using System.Formats.Tar; +using System.IO.Compression; + +using SharedTools; + +namespace Build.Commands; + +public static class Qdrant +{ + public static async Task InstallAsync(RID rid, string version) + { + Console.Write($"- Installing Qdrant {version} for {rid.ToUserFriendlyName()} ..."); + + var cwd = Environment.GetRustRuntimeDirectory(); + var qdrantTmpDownloadPath = Path.GetTempFileName(); + var qdrantTmpExtractPath = Directory.CreateTempSubdirectory(); + var qdrantUrl = GetQdrantDownloadUrl(rid, version); + + // + // Download the file: + // + Console.Write(" downloading ..."); + using (var client = new HttpClient()) + { + var response = await client.GetAsync(qdrantUrl); + if (!response.IsSuccessStatusCode) + { + Console.WriteLine($" failed to download Qdrant {version} for {rid.ToUserFriendlyName()} from {qdrantUrl}"); + return; + } + + await using var fileStream = File.Create(qdrantTmpDownloadPath); + await response.Content.CopyToAsync(fileStream); + } + + // + // Extract the downloaded file: + // + Console.Write(" extracting ..."); + await using(var zStream = File.Open(qdrantTmpDownloadPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + if (rid == RID.WIN_X64) + { + using var archive = new ZipArchive(zStream, ZipArchiveMode.Read); + archive.ExtractToDirectory(qdrantTmpExtractPath.FullName, overwriteFiles: true); + } else + { + await using var uncompressedStream = new GZipStream(zStream, CompressionMode.Decompress); + await TarFile.ExtractToDirectoryAsync(uncompressedStream, qdrantTmpExtractPath.FullName, true); + } + } + + // + // Copy the database to the target directory: + // + Console.Write(" deploying ..."); + var database = GetDatabasePath(rid); + if (string.IsNullOrWhiteSpace(database.Path)) + { + Console.WriteLine($" failed to find the database path for {rid.ToUserFriendlyName()}"); + return; + } + + var qdrantDBSourcePath = Path.Join(qdrantTmpExtractPath.FullName, database.Path); + var qdrantDBTargetPath = Path.Join(cwd, "resources", "databases", "qdrant",database.Filename); + if (!File.Exists(qdrantDBSourcePath)) + { + Console.WriteLine($" failed to find the database file '{qdrantDBSourcePath}'"); + return; + } + + Directory.CreateDirectory(Path.Join(cwd, "resources", "databases", "qdrant")); + if (File.Exists(qdrantDBTargetPath)) + File.Delete(qdrantDBTargetPath); + + File.Copy(qdrantDBSourcePath, qdrantDBTargetPath); + + // + // Cleanup: + // + Console.Write(" cleaning up ..."); + File.Delete(qdrantTmpDownloadPath); + Directory.Delete(qdrantTmpExtractPath.FullName, true); + + Console.WriteLine(" done."); + } + + private static Database GetDatabasePath(RID rid) => rid switch + { + RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"), + RID.LINUX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"), + + RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-gnu"), + RID.OSX_X64 => new("qdrant", "qdrant-x86_64-unknown-linux-gnu"), + + RID.WIN_X64 => new("qdrant.exe", "qdrant-x86_64-pc-windows-msvc.exe"), + + _ => new(string.Empty, string.Empty), + }; + + private static string GetQdrantDownloadUrl(RID rid, string version) + { + var baseUrl = $"https://github.com/qdrant/qdrant/releases/download/{version}/qdrant-"; + return rid switch + { + RID.LINUX_ARM64 => $"{baseUrl}aarch64-unknown-linux-musl.tar.gz", + RID.LINUX_X64 => $"{baseUrl}x86_64-unknown-linux-gnu.tar.gz", + + RID.OSX_ARM64 => $"{baseUrl}aarch64-apple-darwin.tar.gz", + RID.OSX_X64 => $"{baseUrl}x86_64-apple-darwin.tar.gz", + + RID.WIN_X64 => $"{baseUrl}x86_64-pc-windows-msvc.zip", + #warning We have to handle Qdrant for Windows ARM + + _ => string.Empty, + }; + } +} \ No newline at end of file diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs index 06910b45d..cc5b6783b 100644 --- a/app/Build/Commands/UpdateMetadataCommands.cs +++ b/app/Build/Commands/UpdateMetadataCommands.cs @@ -112,6 +112,9 @@ public async Task Build() var pdfiumVersion = await this.ReadPdfiumVersion(); await Pdfium.InstallAsync(rid, pdfiumVersion); + + var qdrantVersion = await this.ReadQdrantVersion(); + await Qdrant.InstallAsync(rid, qdrantVersion); Console.Write($"- Start .NET build for {rid.ToUserFriendlyName()} ..."); await this.ReadCommandOutput(pathApp, "dotnet", $"clean --configuration release --runtime {rid.AsMicrosoftRid()}"); @@ -324,6 +327,16 @@ private async Task ReadPdfiumVersion() return shortVersion; } + private async Task ReadQdrantVersion() + { + const int QDRANT_VERSION_INDEX = 11; + var pathMetadata = Environment.GetMetadataPath(); + var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8); + var currentQdrantVersion = lines[QDRANT_VERSION_INDEX].Trim(); + + return currentQdrantVersion; + } + private async Task UpdateArchitecture(RID rid) { const int ARCHITECTURE_INDEX = 9; diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index c2b1cc0bc..fd41d6c81 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4567,6 +4567,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2777988282"] = "Code in the Rust langu -- Show Details UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T27924674"] = "Show Details" +-- Used Qdrant version +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2799791022"] = "Used Qdrant version" + -- View our project roadmap and help shape AI Studio's future development. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2829971158"] = "View our project roadmap and help shape AI Studio's future development." @@ -4675,6 +4678,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T855925638"] = "We use this library to -- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T870640199"] = "For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose." +-- Qdrant is a vector similarity search engine and vector database. It provides a production-ready service with a convenient API to store, search, and manage points—vectors with an additional payload Qdrant is tailored to extended filtering support. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T95576615"] = "Qdrant is a vector similarity search engine and vector database. It provides a production-ready service with a convenient API to store, search, and manage points—vectors with an additional payload Qdrant is tailored to extended filtering support." + -- Install Pandoc UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T986578435"] = "Install Pandoc" diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index b559389c4..b4b16cd2f 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -87,6 +87,7 @@ $([System.String]::Copy( $(Metadata) ).Split( ';' )[ 8 ]) $([System.String]::Copy( $(Metadata) ).Split( ';' )[ 9 ]) $([System.String]::Copy( $(Metadata) ).Split( ';' )[ 10 ]) + $([System.String]::Copy( $(Metadata) ).Split( ';' )[ 11 ]) true @@ -114,6 +115,9 @@ <_Parameter1>$(MetaPdfiumVersion) + + <_Parameter1>$(MetaQdrantVersion) + diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor index cf66b9009..92f62f6cc 100644 --- a/app/MindWork AI Studio/Pages/About.razor +++ b/app/MindWork AI Studio/Pages/About.razor @@ -19,6 +19,7 @@ + @@ -194,6 +195,7 @@ + diff --git a/app/MindWork AI Studio/Pages/About.razor.cs b/app/MindWork AI Studio/Pages/About.razor.cs index ecdf1d177..e5d7903fa 100644 --- a/app/MindWork AI Studio/Pages/About.razor.cs +++ b/app/MindWork AI Studio/Pages/About.razor.cs @@ -30,6 +30,7 @@ public partial class About : 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 string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(About).Namespace, nameof(About)); @@ -53,6 +54,8 @@ public partial class About : MSGComponentBase private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}"; + private string VersionQdrant => $"{T("Used Qdrant version")}: {META_DATA_DATABASES.QdrantVersion}"; + private string versionPandoc = TB("Determine Pandoc version, please wait..."); private PandocInstallation pandocInstallation; diff --git a/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs b/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs new file mode 100644 index 000000000..9ab92940f --- /dev/null +++ b/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs @@ -0,0 +1,6 @@ +namespace AIStudio.Tools.Metadata; + +public class MetaDataDatabasesAttribute(string qdrantVersion) : Attribute +{ + public string QdrantVersion => qdrantVersion; +} \ No newline at end of file diff --git a/metadata.txt b/metadata.txt index 8b62056f0..62e8ccb4b 100644 --- a/metadata.txt +++ b/metadata.txt @@ -8,4 +8,5 @@ 1.8.1 009bb33d839, release osx-arm64 -137.0.7215.0 \ No newline at end of file +137.0.7215.0 +v1.16.1 \ No newline at end of file diff --git a/runtime/resources/databases/qdrant/config.yaml b/runtime/resources/databases/qdrant/config.yaml new file mode 100644 index 000000000..efdb79daa --- /dev/null +++ b/runtime/resources/databases/qdrant/config.yaml @@ -0,0 +1,353 @@ +log_level: INFO + +# Logging configuration +# Qdrant logs to stdout. You may configure to also write logs to a file on disk. +# Be aware that this file may grow indefinitely. +# logger: +# # Logging format, supports `text` and `json` +# format: text +# on_disk: +# enabled: true +# log_file: path/to/log/file.log +# log_level: INFO +# # Logging format, supports `text` and `json` +# format: text +# buffer_size_bytes: 1024 + +storage: + + snapshots_config: + # "local" or "s3" - where to store snapshots + snapshots_storage: local + # s3_config: + # bucket: "" + # region: "" + # access_key: "" + # secret_key: "" + + # Where to store temporary files + # If null, temporary snapshots are stored in: storage/snapshots_temp/ + temp_path: null + + # If true - point payloads will not be stored in memory. + # It will be read from the disk every time it is requested. + # This setting saves RAM by (slightly) increasing the response time. + # Note: those payload values that are involved in filtering and are indexed - remain in RAM. + # + # Default: true + on_disk_payload: true + + # Maximum number of concurrent updates to shard replicas + # If `null` - maximum concurrency is used. + update_concurrency: null + + # Write-ahead-log related configuration + wal: + # Size of a single WAL segment + wal_capacity_mb: 32 + + # Number of WAL segments to create ahead of actual data requirement + wal_segments_ahead: 0 + + # Normal node - receives all updates and answers all queries + node_type: "Normal" + + # Listener node - receives all updates, but does not answer search/read queries + # Useful for setting up a dedicated backup node + # node_type: "Listener" + + performance: + # Number of parallel threads used for search operations. If 0 - auto selection. + max_search_threads: 0 + + # CPU budget, how many CPUs (threads) to allocate for an optimization job. + # If 0 - auto selection, keep 1 or more CPUs unallocated depending on CPU size + # If negative - subtract this number of CPUs from the available CPUs. + # If positive - use this exact number of CPUs. + optimizer_cpu_budget: 0 + + # Prevent DDoS of too many concurrent updates in distributed mode. + # One external update usually triggers multiple internal updates, which breaks internal + # timings. For example, the health check timing and consensus timing. + # If null - auto selection. + update_rate_limit: null + + # Limit for number of incoming automatic shard transfers per collection on this node, does not affect user-requested transfers. + # The same value should be used on all nodes in a cluster. + # Default is to allow 1 transfer. + # If null - allow unlimited transfers. + #incoming_shard_transfers_limit: 1 + + # Limit for number of outgoing automatic shard transfers per collection on this node, does not affect user-requested transfers. + # The same value should be used on all nodes in a cluster. + # Default is to allow 1 transfer. + # If null - allow unlimited transfers. + #outgoing_shard_transfers_limit: 1 + + # Enable async scorer which uses io_uring when rescoring. + # Only supported on Linux, must be enabled in your kernel. + # See: + #async_scorer: false + + optimizers: + # The minimal fraction of deleted vectors in a segment, required to perform segment optimization + deleted_threshold: 0.2 + + # The minimal number of vectors in a segment, required to perform segment optimization + vacuum_min_vector_number: 1000 + + # Target amount of segments optimizer will try to keep. + # Real amount of segments may vary depending on multiple parameters: + # - Amount of stored points + # - Current write RPS + # + # It is recommended to select default number of segments as a factor of the number of search threads, + # so that each segment would be handled evenly by one of the threads. + # If `default_segment_number = 0`, will be automatically selected by the number of available CPUs + default_segment_number: 0 + + # Do not create segments larger this size (in KiloBytes). + # Large segments might require disproportionately long indexation times, + # therefore it makes sense to limit the size of segments. + # + # If indexation speed have more priority for your - make this parameter lower. + # If search speed is more important - make this parameter higher. + # Note: 1Kb = 1 vector of size 256 + # If not set, will be automatically selected considering the number of available CPUs. + max_segment_size_kb: null + + # Maximum size (in KiloBytes) of vectors allowed for plain index. + # Default value based on experiments and observations. + # Note: 1Kb = 1 vector of size 256 + # To explicitly disable vector indexing, set to `0`. + # If not set, the default value will be used. + indexing_threshold_kb: 10000 + + # Interval between forced flushes. + flush_interval_sec: 5 + + # Max number of threads (jobs) for running optimizations per shard. + # Note: each optimization job will also use `max_indexing_threads` threads by itself for index building. + # If null - have no limit and choose dynamically to saturate CPU. + # If 0 - no optimization threads, optimizations will be disabled. + max_optimization_threads: null + + # This section has the same options as 'optimizers' above. All values specified here will overwrite the collections + # optimizers configs regardless of the config above and the options specified at collection creation. + #optimizers_overwrite: + # deleted_threshold: 0.2 + # vacuum_min_vector_number: 1000 + # default_segment_number: 0 + # max_segment_size_kb: null + # indexing_threshold_kb: 10000 + # flush_interval_sec: 5 + # max_optimization_threads: null + + # Default parameters of HNSW Index. Could be overridden for each collection or named vector individually + hnsw_index: + # Number of edges per node in the index graph. Larger the value - more accurate the search, more space required. + m: 16 + + # Number of neighbours to consider during the index building. Larger the value - more accurate the search, more time required to build index. + ef_construct: 100 + + # Minimal size threshold (in KiloBytes) below which full-scan is preferred over HNSW search. + # This measures the total size of vectors being queried against. + # When the maximum estimated amount of points that a condition satisfies is smaller than + # `full_scan_threshold_kb`, the query planner will use full-scan search instead of HNSW index + # traversal for better performance. + # Note: 1Kb = 1 vector of size 256 + full_scan_threshold_kb: 10000 + + # Number of parallel threads used for background index building. + # If 0 - automatically select. + # Best to keep between 8 and 16 to prevent likelihood of building broken/inefficient HNSW graphs. + # On small CPUs, less threads are used. + max_indexing_threads: 0 + + # Store HNSW index on disk. If set to false, index will be stored in RAM. Default: false + on_disk: false + + # Custom M param for hnsw graph built for payload index. If not set, default M will be used. + payload_m: null + + # Default shard transfer method to use if none is defined. + # If null - don't have a shard transfer preference, choose automatically. + # If stream_records, snapshot or wal_delta - prefer this specific method. + # More info: https://qdrant.tech/documentation/guides/distributed_deployment/#shard-transfer-method + shard_transfer_method: null + + # Default parameters for collections + collection: + # Number of replicas of each shard that network tries to maintain + replication_factor: 1 + + # How many replicas should apply the operation for us to consider it successful + write_consistency_factor: 1 + + # Default parameters for vectors. + vectors: + # Whether vectors should be stored in memory or on disk. + on_disk: null + + # shard_number_per_node: 1 + + # Default quantization configuration. + # More info: https://qdrant.tech/documentation/guides/quantization + quantization: null + + # Default strict mode parameters for newly created collections. + #strict_mode: + # Whether strict mode is enabled for a collection or not. + #enabled: false + + # Max allowed `limit` parameter for all APIs that don't have their own max limit. + #max_query_limit: null + + # Max allowed `timeout` parameter. + #max_timeout: null + + # Allow usage of unindexed fields in retrieval based (eg. search) filters. + #unindexed_filtering_retrieve: null + + # Allow usage of unindexed fields in filtered updates (eg. delete by payload). + #unindexed_filtering_update: null + + # Max HNSW value allowed in search parameters. + #search_max_hnsw_ef: null + + # Whether exact search is allowed or not. + #search_allow_exact: null + + # Max oversampling value allowed in search. + #search_max_oversampling: null + + # Maximum number of collections allowed to be created + # If null - no limit. + max_collections: null + +service: + # Maximum size of POST data in a single request in megabytes + max_request_size_mb: 32 + + # Number of parallel workers used for serving the api. If 0 - equal to the number of available cores. + # If missing - Same as storage.max_search_threads + max_workers: 0 + + # Host to bind the service on + host: 0.0.0.0 + + # HTTP(S) port to bind the service on + http_port: 6373 + + # gRPC port to bind the service on. + # If `null` - gRPC is disabled. Default: null + # Comment to disable gRPC: + grpc_port: 6344 + + # Enable CORS headers in REST API. + # If enabled, browsers would be allowed to query REST endpoints regardless of query origin. + # More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS + # Default: true + enable_cors: true + + # Enable HTTPS for the REST and gRPC API + enable_tls: false + + # Check user HTTPS client certificate against CA file specified in tls config + verify_https_client_certificate: false + + # Set an api-key. + # If set, all requests must include a header with the api-key. + # example header: `api-key: ` + # + # If you enable this you should also enable TLS. + # (Either above or via an external service like nginx.) + # Sending an api-key over an unencrypted channel is insecure. + # + # Uncomment to enable. + # api_key: your_secret_api_key_here + + # Set an api-key for read-only operations. + # If set, all requests must include a header with the api-key. + # example header: `api-key: ` + # + # If you enable this you should also enable TLS. + # (Either above or via an external service like nginx.) + # Sending an api-key over an unencrypted channel is insecure. + # + # Uncomment to enable. + # read_only_api_key: your_secret_read_only_api_key_here + + # Uncomment to enable JWT Role Based Access Control (RBAC). + # If enabled, you can generate JWT tokens with fine-grained rules for access control. + # Use generated token instead of API key. + # + # jwt_rbac: true + + # Hardware reporting adds information to the API responses with a + # hint on how many resources were used to execute the request. + # + # Warning: experimental, this feature is still under development and is not supported yet. + # + # Uncomment to enable. + # hardware_reporting: true + # + # Uncomment to enable. + # Prefix for the names of metrics in the /metrics API. + # metrics_prefix: qdrant_ + +cluster: + # Use `enabled: true` to run Qdrant in distributed deployment mode + enabled: false + + # Configuration of the inter-cluster communication + p2p: + # Port for internal communication between peers + port: 6335 + + # Use TLS for communication between peers + enable_tls: false + + # Configuration related to distributed consensus algorithm + consensus: + # How frequently peers should ping each other. + # Setting this parameter to lower value will allow consensus + # to detect disconnected nodes earlier, but too frequent + # tick period may create significant network and CPU overhead. + # We encourage you NOT to change this parameter unless you know what you are doing. + tick_period_ms: 100 + + # Compact consensus operations once we have this amount of applied + # operations. Allows peers to join quickly with a consensus snapshot without + # replaying a huge amount of operations. + # If 0 - disable compaction + compact_wal_entries: 128 + +# Set to true to prevent service from sending usage statistics to the developers. +# Read more: https://qdrant.tech/documentation/guides/telemetry +telemetry_disabled: false + +# 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 dd9944156..7cd97b8bc 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -17,6 +17,7 @@ use crate::dotnet::stop_dotnet_server; use crate::environment::{is_prod, is_dev, CONFIG_DIRECTORY, DATA_DIRECTORY}; use crate::log::switch_to_file_logging; use crate::pdfium::PDFIUM_LIB_PATH; +use crate::qdrant::start_qdrant_server; /// The Tauri main window. static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)); @@ -94,6 +95,9 @@ pub fn start_tauri() { info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}"); switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap(); set_pdfium_path(app.path_resolver()); + + start_qdrant_server(); + Ok(()) }) .plugin(tauri_plugin_window_state::Builder::default().build()) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7868a7a44..bd7da3078 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -12,4 +12,5 @@ pub mod certificate; pub mod file_data; pub mod metadata; pub mod pdfium; -pub mod pandoc; \ No newline at end of file +pub mod pandoc; +pub mod qdrant; \ No newline at end of file diff --git a/runtime/src/main.rs b/runtime/src/main.rs index a66ee2878..34620a911 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -38,6 +38,7 @@ async fn main() { info!(".. MudBlazor: v{mud_blazor_version}", mud_blazor_version = metadata.mud_blazor_version); info!(".. Tauri: v{tauri_version}", tauri_version = metadata.tauri_version); info!(".. PDFium: v{pdfium_version}", pdfium_version = metadata.pdfium_version); + info!(".. Qdrant: {qdrant_version}", qdrant_version = metadata.qdrant_version); if is_dev() { warn!("Running in development mode."); diff --git a/runtime/src/metadata.rs b/runtime/src/metadata.rs index 426e2b666..fa56dd687 100644 --- a/runtime/src/metadata.rs +++ b/runtime/src/metadata.rs @@ -16,6 +16,7 @@ pub struct MetaData { pub app_commit_hash: String, pub architecture: String, pub pdfium_version: String, + pub qdrant_version: String, } impl MetaData { @@ -39,6 +40,7 @@ impl MetaData { let app_commit_hash = metadata_lines.next().unwrap(); let architecture = metadata_lines.next().unwrap(); let pdfium_version = metadata_lines.next().unwrap(); + let qdrant_version = metadata_lines.next().unwrap(); let metadata = MetaData { architecture: architecture.to_string(), @@ -52,6 +54,7 @@ impl MetaData { rust_version: rust_version.to_string(), tauri_version: tauri_version.to_string(), pdfium_version: pdfium_version.to_string(), + qdrant_version: qdrant_version.to_string(), }; *META_DATA.lock().unwrap() = Some(metadata.clone()); diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs new file mode 100644 index 000000000..f95ebcd16 --- /dev/null +++ b/runtime/src/qdrant.rs @@ -0,0 +1,122 @@ +use std::collections::HashMap; +use std::path::Path; +use std::sync::{Arc, Mutex}; +use log::{debug, error, info, warn}; +use once_cell::sync::Lazy; +use rocket::get; +use tauri::api::process::{Command, CommandChild, CommandEvent}; +use tauri::Url; +use crate::api_token::{APIToken}; +use crate::environment::DATA_DIRECTORY; + +// Qdrant server process started in a separate process and can communicate +// via HTTP or gRPC with the .NET server and the runtime process +static QDRANT_SERVER: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); + +// Qdrant server port (default is 6333 for HTTP and 6334 for gRPC) +static QDRANT_SERVER_PORT: Lazy = Lazy::new(|| { + crate::network::get_available_port().unwrap_or(6333) +}); + +static QDRANT_INITIALIZED: Lazy> = Lazy::new(|| Mutex::new(false)); + +#[get("/system/qdrant/port")] +pub fn qdrant_port(_token: APIToken) -> String { + let qdrant_port = *QDRANT_SERVER_PORT; + format!("{qdrant_port}") +} + +/// Starts the Qdrant server in a separate process. +pub fn start_qdrant_server() { + + let base_path = DATA_DIRECTORY.get().unwrap(); + + let storage_path = Path::new(base_path).join("databases").join("qdrant").join("storage").to_str().unwrap().to_string(); + let snapshot_path = Path::new(base_path).join("databases").join("qdrant").join("snapshots").to_str().unwrap().to_string(); + let init_path = Path::new(base_path).join("databases").join("qdrant").join(".qdrant-initalized").to_str().unwrap().to_string(); + + println!("{}", storage_path); + println!("{}", snapshot_path); + println!("{}", init_path); + + let qdrant_server_environment = HashMap::from_iter([ + (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT.to_string()), + (String::from("QDRANT_INIT_FILE_PATH"), init_path), + (String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path), + (String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path), + ]); + let server_spawn_clone = QDRANT_SERVER.clone(); + tauri::async_runtime::spawn(async move { + let (mut rx, child) = Command::new_sidecar("qdrant") + .expect("Failed to create sidecar for Qdrant") + .args(["--config-path", "resources/databases/qdrant/config.yaml"]) + .envs(qdrant_server_environment) + .spawn() + .expect("Failed to spawn Qdrant server process."); + + let server_pid = child.pid(); + info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}."); + + // Save the server process to stop it later: + *server_spawn_clone.lock().unwrap() = Some(child); + + // Log the output of the Qdrant server: + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(line) => { + let line = line.trim_end(); + if line.contains("INFO") || line.contains("info") { + info!(Source = "Qdrant Server"; "{line}"); + } else if line.contains("WARN") || line.contains("warning") { + warn!(Source = "Qdrant Server"; "{line}"); + } else if line.contains("ERROR") || line.contains("error") { + error!(Source = "Qdrant Server"; "{line}"); + } else { + debug!(Source = "Qdrant Server"; "{line}"); + } + } + CommandEvent::Stderr(line) => { + error!(Source = "Qdrant Server (stderr)"; "{line}"); + } + _ => {} + } + } + }); +} + +/// This endpoint is called by the Qdrant server or frontend to signal that Qdrant is ready. +#[get("/system/qdrant/ready")] +pub async fn qdrant_ready(_token: APIToken) { + { + let mut initialized = QDRANT_INITIALIZED.lock().unwrap(); + if *initialized { + warn!("Qdrant server was already initialized."); + return; + } + info!("Qdrant server is ready."); + *initialized = true; + } + + let qdrant_port = *QDRANT_SERVER_PORT; + let _url = match Url::parse(format!("http://localhost:{qdrant_port}").as_str()) { + Ok(url) => url, + Err(msg) => { + error!("Error while parsing Qdrant URL: {msg}"); + return; + } + }; + +} + +/// Stops the Qdrant server process. +pub fn stop_qdrant_server() { + if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() { + let server_kill_result = server_process.kill(); + match server_kill_result { + Ok(_) => info!("Qdrant server process was stopped."), + Err(e) => error!("Failed to stop Qdrant server process: {e}."), + } + } else { + warn!("Qdrant server process was not started or is already stopped."); + } +} \ No newline at end of file diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index ef116adda..d2cb54f36 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -20,6 +20,11 @@ "name": "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", "sidecar": true, "args": true + }, + { + "name": "resources/databases/qdrant/qdrant", + "sidecar": true, + "args": true } ] }, @@ -59,7 +64,8 @@ "targets": "all", "identifier": "com.github.mindwork-ai.ai-studio", "externalBin": [ - "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer" + "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", + "resources/databases/qdrant/qdrant" ], "resources": [ "resources/*" From a79a2996fef04dea31d9df06166069f812814059 Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Thu, 11 Dec 2025 14:40:08 +0100 Subject: [PATCH 2/2] Added pipeline to retrieve information for .NET runtime to create Qdrant client --- .github/workflows/build-and-release.yml | 8 +-- README.md | 2 +- app/Build/Commands/Qdrant.cs | 10 +-- .../Assistants/I18N/allTexts.lua | 9 ++- app/MindWork AI Studio/Pages/About.razor | 24 ++++++- app/MindWork AI Studio/Pages/About.razor.cs | 13 +++- app/MindWork AI Studio/Program.cs | 23 ++++++ app/MindWork AI Studio/Settings/Profile.cs | 1 - .../Tools/Databases/DatabaseClient.cs | 71 +++++++++++++++++++ .../Tools/Databases/Qdrant/QdrantClient.cs | 15 ++++ .../Metadata/MetaDataDatabasesAttribute.cs | 4 +- .../Tools/Rust/QdrantInfo.cs | 13 ++++ .../Tools/Services/RustService.Databases.cs | 26 +++++++ .../wwwroot/changelog/v0.9.55.md | 1 + metadata.txt | 2 +- .../resources/databases/qdrant/config.yaml | 8 +-- runtime/src/main.rs | 2 +- runtime/src/qdrant.rs | 59 ++++++--------- runtime/src/runtime_api.rs | 1 + 19 files changed, 232 insertions(+), 60 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs create mode 100644 app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClient.cs create mode 100644 app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs create mode 100644 app/MindWork AI Studio/Tools/Services/RustService.Databases.cs diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 0d47e4d23..8cf531b83 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -174,7 +174,7 @@ jobs: pdfium_version=$(echo $pdfium_version | cut -d'.' -f3) # Next line is the Qdrant version: - qdrant_version=$(sed -n '12p' metadata.txt) + qdrant_version="v$(sed -n '12p' metadata.txt)" # Write the metadata to the environment: echo "APP_VERSION=${app_version}" >> $GITHUB_ENV @@ -247,7 +247,7 @@ jobs: $pdfium_version = $pdfium_version.Split('.')[2] # Next line is the necessary Qdrant version: - $qdrant_version = $metadata[12] + $qdrant_version = "v$metadata[12]" # Write the metadata to the environment: Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV @@ -441,7 +441,7 @@ jobs: ;; esac - QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSON}/qdrant-{QDRANT_FILE}" + QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/v${QDRANT_VERSION}/qdrant-{QDRANT_FILE}" echo "Download Qdrant $QDRANT_URL ..." TMP=$(mktemp -d) @@ -485,7 +485,7 @@ jobs: } } - QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSON}/qdrant-{QDRANT_FILE}" + QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/v${QDRANT_VERSION}/qdrant-{QDRANT_FILE}" Write-Host "Download $QDRANT_URL ..." # Create a unique temporary directory (not just a file) diff --git a/README.md b/README.md index d526d2b3f..e8489aef6 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Since November 2024: Work on RAG (integration of your data and files) has begun. - [x] ~~App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393), [PR #487](https://github.com/MindWorkAI/AI-Studio/pull/487))~~ - [ ] App: Implement external embedding providers - [ ] App: Implement the process to vectorize one local file using embeddings -- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb) +- [ ] Runtime: Integration of the vector database [Qdrant](https://github.com/qdrant/qdrant) - [ ] App: Implement the continuous process of vectorizing data - [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~ - [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~ diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs index cc0bb1e4e..9a573823d 100644 --- a/app/Build/Commands/Qdrant.cs +++ b/app/Build/Commands/Qdrant.cs @@ -88,11 +88,11 @@ public static async Task InstallAsync(RID rid, string version) private static Database GetDatabasePath(RID rid) => rid switch { - RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"), - RID.LINUX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"), + RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"), + RID.OSX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"), - RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-gnu"), - RID.OSX_X64 => new("qdrant", "qdrant-x86_64-unknown-linux-gnu"), + RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-gnu"), + RID.LINUX_X64 => new("qdrant", "qdrant-x86_64-unknown-linux-gnu"), RID.WIN_X64 => new("qdrant.exe", "qdrant-x86_64-pc-windows-msvc.exe"), @@ -101,7 +101,7 @@ public static async Task InstallAsync(RID rid, string version) private static string GetQdrantDownloadUrl(RID rid, string version) { - var baseUrl = $"https://github.com/qdrant/qdrant/releases/download/{version}/qdrant-"; + var baseUrl = $"https://github.com/qdrant/qdrant/releases/download/v{version}/qdrant-"; return rid switch { RID.LINUX_ARM64 => $"{baseUrl}aarch64-unknown-linux-musl.tar.gz", diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index fd41d6c81..8ffac5d96 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4462,6 +4462,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1282228996"] = "AI Studio runs with an -- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat." +-- Database version +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1420062548"] = "Database version" + -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library." @@ -4501,6 +4504,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1924365263"] = "This library is used t -- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust." +-- Copies the following to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2029659664"] = "Copies the following to the clipboard" + -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2037899437"] = "Copies the server URL to the clipboard" @@ -4567,9 +4573,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2777988282"] = "Code in the Rust langu -- Show Details UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T27924674"] = "Show Details" --- Used Qdrant version -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2799791022"] = "Used Qdrant version" - -- View our project roadmap and help shape AI Studio's future development. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2829971158"] = "View our project roadmap and help shape AI Studio's future development." diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor index 92f62f6cc..d9fd748b4 100644 --- a/app/MindWork AI Studio/Pages/About.razor +++ b/app/MindWork AI Studio/Pages/About.razor @@ -19,7 +19,29 @@ - + + + @this.VersionDatabase + + + + @foreach (var (Label, Value) in DatabaseClient.GetDisplayInfo()) + { +
+ + @Label: @Value + +
+ } +
+
+ + @(this.showDatabaseDetails ? T("Hide Details") : T("Show Details")) + +
diff --git a/app/MindWork AI Studio/Pages/About.razor.cs b/app/MindWork AI Studio/Pages/About.razor.cs index e5d7903fa..9371c610d 100644 --- a/app/MindWork AI Studio/Pages/About.razor.cs +++ b/app/MindWork AI Studio/Pages/About.razor.cs @@ -2,6 +2,7 @@ using AIStudio.Components; using AIStudio.Dialogs; +using AIStudio.Tools.Databases; using AIStudio.Tools.Metadata; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Rust; @@ -26,6 +27,9 @@ public partial class About : MSGComponentBase [Inject] private ISnackbar Snackbar { get; init; } = null!; + [Inject] + private DatabaseClient DatabaseClient { get; init; } = null!; + private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly(); private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute()!; private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute()!; @@ -54,7 +58,7 @@ public partial class About : MSGComponentBase private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}"; - private string VersionQdrant => $"{T("Used Qdrant version")}: {META_DATA_DATABASES.QdrantVersion}"; + private string VersionDatabase => $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}"; private string versionPandoc = TB("Determine Pandoc version, please wait..."); private PandocInstallation pandocInstallation; @@ -63,6 +67,8 @@ public partial class About : MSGComponentBase private bool showEnterpriseConfigDetails; + private bool showDatabaseDetails = false; + private IPluginMetadata? configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION); /// @@ -173,6 +179,11 @@ private void ToggleEnterpriseConfigDetails() { this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails; } + + private void ToggleDatabaseDetails() + { + this.showDatabaseDetails = !this.showDatabaseDetails; + } private async Task CopyStartupLogPath() { diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 2514a67fe..07439d06d 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -1,6 +1,9 @@ using AIStudio.Agents; using AIStudio.Settings; +using AIStudio.Tools.Databases; +using AIStudio.Tools.Databases.Qdrant; using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.Rust; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -82,6 +85,24 @@ public static async Task Main() return; } + var qdrantInfo = await rust.GetQdrantInfo(); + if (qdrantInfo.Path == String.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant path from Rust."); + return; + } + if (qdrantInfo.PortHttp == 0) + { + Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust."); + return; + } + + if (qdrantInfo.PortGrpc == 0) + { + Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust."); + return; + } + var builder = WebApplication.CreateBuilder(); builder.WebHost.ConfigureKestrel(kestrelServerOptions => @@ -133,6 +154,7 @@ public static async Task Main() builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); + builder.Services.AddSingleton(new QdrantClient("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc)); // ReSharper disable AccessToDisposedClosure builder.Services.AddHostedService(_ => rust); @@ -215,6 +237,7 @@ public static async Task Main() await rust.AppIsReady(); programLogger.LogInformation("The AI Studio server is ready."); + TaskScheduler.UnobservedTaskException += (sender, taskArgs) => { programLogger.LogError(taskArgs.Exception, $"Unobserved task exception by sender '{sender ?? "n/a"}'."); diff --git a/app/MindWork AI Studio/Settings/Profile.cs b/app/MindWork AI Studio/Settings/Profile.cs index 0436beb5a..2e9dc80a0 100644 --- a/app/MindWork AI Studio/Settings/Profile.cs +++ b/app/MindWork AI Studio/Settings/Profile.cs @@ -77,7 +77,6 @@ The user wants you to consider the following things. public static bool TryParseProfileTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject template) { - LOGGER.LogInformation($"\n Profile table parsing {idx}.\n"); template = NO_PROFILE; if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) { diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs new file mode 100644 index 000000000..0ca84e013 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs @@ -0,0 +1,71 @@ +namespace AIStudio.Tools.Databases; + +public abstract class DatabaseClient +{ + public string Name { get; } + private string Path { get; } + + public DatabaseClient(string name, string path) + { + this.Name = name; + this.Path = path; + } + + public abstract IEnumerable<(string Label, string Value)> GetDisplayInfo(); + + public string GetStorageSize() + { + if (string.IsNullOrEmpty(this.Path)) + { + Console.WriteLine($"Error: Database path '{this.Path}' cannot be null or empty."); + return "0 B"; + } + + if (!Directory.Exists(this.Path)) + { + Console.WriteLine($"Error: Database path '{this.Path}' does not exist."); + return "0 B"; + } + long size = 0; + var stack = new Stack(); + stack.Push(this.Path); + while (stack.Count > 0) + { + string directory = stack.Pop(); + try + { + var files = Directory.GetFiles(directory); + size += files.Sum(file => new FileInfo(file).Length); + var subDirectories = Directory.GetDirectories(directory); + foreach (var subDirectory in subDirectories) + { + stack.Push(subDirectory); + } + } + catch (UnauthorizedAccessException) + { + Console.WriteLine($"No access to {directory}"); + } + catch (Exception ex) + { + Console.WriteLine($"An error encountered while processing {directory}: "); + Console.WriteLine($"{ ex.Message}"); + } + } + return FormatBytes(size); + } + + public static string FormatBytes(long size) + { + string[] suffixes = { "B", "KB", "MB", "GB", "TB", "PB" }; + int suffixIndex = 0; + + while (size >= 1024 && suffixIndex < suffixes.Length - 1) + { + size /= 1024; + suffixIndex++; + } + + return $"{size:0##} {suffixes[suffixIndex]}"; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClient.cs b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClient.cs new file mode 100644 index 000000000..c3a4fabd3 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClient.cs @@ -0,0 +1,15 @@ +namespace AIStudio.Tools.Databases.Qdrant; + +public class QdrantClient(string name, string path, int httpPort, int grpcPort) : DatabaseClient(name, path) +{ + private int HttpPort { get; } = httpPort; + private int GrpcPort { get; } = grpcPort; + private string IpAddress { get; } = "127.0.0.1"; + + public override IEnumerable<(string Label, string Value)> GetDisplayInfo() + { + yield return ("HTTP Port", this.HttpPort.ToString()); + yield return ("gRPC Port", this.GrpcPort.ToString()); + yield return ("Storage Size", $"{base.GetStorageSize()}"); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs b/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs index 9ab92940f..5ef6064b4 100644 --- a/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs +++ b/app/MindWork AI Studio/Tools/Metadata/MetaDataDatabasesAttribute.cs @@ -1,6 +1,6 @@ namespace AIStudio.Tools.Metadata; -public class MetaDataDatabasesAttribute(string qdrantVersion) : Attribute +public class MetaDataDatabasesAttribute(string databaseVersion) : Attribute { - public string QdrantVersion => qdrantVersion; + public string DatabaseVersion => databaseVersion; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs new file mode 100644 index 000000000..8cbe5e9c8 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs @@ -0,0 +1,13 @@ +namespace AIStudio.Tools.Rust; + +/// +/// The response of the Qdrant information request. +/// +/// The port number for HTTP communication with Qdrant. +/// The port number for gRPC communication with Qdrant +public record struct QdrantInfo +{ + public string Path { get; init; } + public int PortHttp { get; init; } + public int PortGrpc { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs new file mode 100644 index 000000000..ae42316dc --- /dev/null +++ b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs @@ -0,0 +1,26 @@ +using AIStudio.Tools.Rust; + +namespace AIStudio.Tools.Services; + +public sealed partial class RustService +{ + public async Task GetQdrantInfo() + { + try + { + var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45)); + var response = await this.http.GetFromJsonAsync("/system/qdrant/port", this.jsonRustSerializerOptions, cts.Token); + return response; + } + catch (Exception e) + { + Console.WriteLine(e); + return new QdrantInfo + { + Path = string.Empty, + PortHttp = 0, + PortGrpc = 0, + }; + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md index e72bca78b..acc432fc0 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.55.md @@ -1 +1,2 @@ # v0.9.55, build 230 (2025-12-xx xx:xx UTC) +Added functionality to download Qdrant and execute it as a background sidecar. \ No newline at end of file diff --git a/metadata.txt b/metadata.txt index 62e8ccb4b..5746ea973 100644 --- a/metadata.txt +++ b/metadata.txt @@ -9,4 +9,4 @@ 009bb33d839, release osx-arm64 137.0.7215.0 -v1.16.1 \ No newline at end of file +1.16.1 \ No newline at end of file diff --git a/runtime/resources/databases/qdrant/config.yaml b/runtime/resources/databases/qdrant/config.yaml index efdb79daa..267f81c24 100644 --- a/runtime/resources/databases/qdrant/config.yaml +++ b/runtime/resources/databases/qdrant/config.yaml @@ -235,15 +235,15 @@ service: max_workers: 0 # Host to bind the service on - host: 0.0.0.0 + host: 127.0.0.1 # HTTP(S) port to bind the service on - http_port: 6373 + # http_port: 6333 # gRPC port to bind the service on. # If `null` - gRPC is disabled. Default: null # Comment to disable gRPC: - grpc_port: 6344 + # grpc_port: 6334 # Enable CORS headers in REST API. # If enabled, browsers would be allowed to query REST endpoints regardless of query origin. @@ -326,7 +326,7 @@ cluster: # Set to true to prevent service from sending usage statistics to the developers. # Read more: https://qdrant.tech/documentation/guides/telemetry -telemetry_disabled: false +telemetry_disabled: true # TLS configuration. # Required if either service.enable_tls or cluster.p2p.enable_tls is true. diff --git a/runtime/src/main.rs b/runtime/src/main.rs index 34620a911..bfbe4750c 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -38,7 +38,7 @@ async fn main() { info!(".. MudBlazor: v{mud_blazor_version}", mud_blazor_version = metadata.mud_blazor_version); info!(".. Tauri: v{tauri_version}", tauri_version = metadata.tauri_version); info!(".. PDFium: v{pdfium_version}", pdfium_version = metadata.pdfium_version); - info!(".. Qdrant: {qdrant_version}", qdrant_version = metadata.qdrant_version); + info!(".. Qdrant: v{qdrant_version}", qdrant_version = metadata.qdrant_version); if is_dev() { warn!("Running in development mode."); diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index f95ebcd16..3b2b94ced 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -4,8 +4,9 @@ use std::sync::{Arc, Mutex}; use log::{debug, error, info, warn}; use once_cell::sync::Lazy; use rocket::get; +use rocket::serde::json::Json; +use rocket::serde::Serialize; use tauri::api::process::{Command, CommandChild, CommandEvent}; -use tauri::Url; use crate::api_token::{APIToken}; use crate::environment::DATA_DIRECTORY; @@ -14,16 +15,28 @@ use crate::environment::DATA_DIRECTORY; static QDRANT_SERVER: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None))); // Qdrant server port (default is 6333 for HTTP and 6334 for gRPC) -static QDRANT_SERVER_PORT: Lazy = Lazy::new(|| { +static QDRANT_SERVER_PORT_HTTP: Lazy = Lazy::new(|| { crate::network::get_available_port().unwrap_or(6333) -}); +}); -static QDRANT_INITIALIZED: Lazy> = Lazy::new(|| Mutex::new(false)); +static QDRANT_SERVER_PORT_GRPC: Lazy = Lazy::new(|| { + crate::network::get_available_port().unwrap_or(6334) +}); + +#[derive(Serialize)] +pub struct ProvideQdrantInfo { + path: String, + port_http: u16, + port_grpc: u16, +} #[get("/system/qdrant/port")] -pub fn qdrant_port(_token: APIToken) -> String { - let qdrant_port = *QDRANT_SERVER_PORT; - format!("{qdrant_port}") +pub fn qdrant_port(_token: APIToken) -> Json { + return Json(ProvideQdrantInfo { + path: Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").to_str().unwrap().to_string(), + port_http: *QDRANT_SERVER_PORT_HTTP, + port_grpc: *QDRANT_SERVER_PORT_GRPC, + }); } /// Starts the Qdrant server in a separate process. @@ -35,16 +48,14 @@ pub fn start_qdrant_server() { let snapshot_path = Path::new(base_path).join("databases").join("qdrant").join("snapshots").to_str().unwrap().to_string(); let init_path = Path::new(base_path).join("databases").join("qdrant").join(".qdrant-initalized").to_str().unwrap().to_string(); - println!("{}", storage_path); - println!("{}", snapshot_path); - println!("{}", init_path); - let qdrant_server_environment = HashMap::from_iter([ - (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT.to_string()), + (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), + (String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()), (String::from("QDRANT_INIT_FILE_PATH"), init_path), (String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path), (String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path), ]); + let server_spawn_clone = QDRANT_SERVER.clone(); tauri::async_runtime::spawn(async move { let (mut rx, child) = Command::new_sidecar("qdrant") @@ -84,30 +95,6 @@ pub fn start_qdrant_server() { }); } -/// This endpoint is called by the Qdrant server or frontend to signal that Qdrant is ready. -#[get("/system/qdrant/ready")] -pub async fn qdrant_ready(_token: APIToken) { - { - let mut initialized = QDRANT_INITIALIZED.lock().unwrap(); - if *initialized { - warn!("Qdrant server was already initialized."); - return; - } - info!("Qdrant server is ready."); - *initialized = true; - } - - let qdrant_port = *QDRANT_SERVER_PORT; - let _url = match Url::parse(format!("http://localhost:{qdrant_port}").as_str()) { - Ok(url) => url, - Err(msg) => { - error!("Error while parsing Qdrant URL: {msg}"); - return; - } - }; - -} - /// Stops the Qdrant server process. pub fn stop_qdrant_server() { if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() { diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 23fc5e33e..529d96369 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -67,6 +67,7 @@ pub fn start_runtime_api() { .mount("/", routes![ crate::dotnet::dotnet_port, crate::dotnet::dotnet_ready, + crate::qdrant::qdrant_port, crate::clipboard::set_clipboard, crate::app_window::get_event_stream, crate::app_window::check_for_update,