From dec2c27997fea7c7d4630e1dd9aedce98d62b01a Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Wed, 3 Dec 2025 17:21:25 +0100 Subject: [PATCH 01/30] 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 02/30] 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, From 1dcfd19f722c4558cae8c4420c9425a4dac553f8 Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Tue, 13 Jan 2026 16:38:22 +0100 Subject: [PATCH 03/30] Added TLS and API token support for Qdrant communication. --- .github/workflows/build-and-release.yml | 9 ++- app/Build/Commands/Qdrant.cs | 5 +- .../MindWork AI Studio.csproj | 1 + app/MindWork AI Studio/Pages/About.razor | 6 +- app/MindWork AI Studio/Pages/About.razor.cs | 7 ++ app/MindWork AI Studio/Program.cs | 23 +++++- .../Tools/Databases/DatabaseClient.cs | 57 +++++---------- .../Tools/Databases/Qdrant/QdrantClient.cs | 15 ---- .../Qdrant/QdrantClientImplementation.cs | 61 ++++++++++++++++ .../Tools/Rust/QdrantInfo.cs | 2 + .../Tools/Services/RustService.Databases.cs | 4 +- runtime/Cargo.toml | 1 + .../resources/databases/qdrant/config.yaml | 4 +- runtime/src/api_token.rs | 58 +++------------ runtime/src/app_window.rs | 3 +- runtime/src/certificate.rs | 38 ---------- runtime/src/certificate_factory.rs | 22 ++++++ runtime/src/dotnet.rs | 5 +- runtime/src/lib.rs | 6 +- runtime/src/main.rs | 4 +- runtime/src/qdrant.rs | 71 +++++++++++++++++-- runtime/src/runtime_api.rs | 2 +- runtime/src/runtime_api_token.rs | 40 +++++++++++ runtime/src/runtime_certificate.rs | 26 +++++++ 24 files changed, 302 insertions(+), 168 deletions(-) delete mode 100644 app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClient.cs create mode 100644 app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs delete mode 100644 runtime/src/certificate.rs create mode 100644 runtime/src/certificate_factory.rs create mode 100644 runtime/src/runtime_api_token.rs create mode 100644 runtime/src/runtime_certificate.rs diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 8cf531b83..cf8daf457 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -344,7 +344,7 @@ jobs: echo "Cleaning up ..." rm -fr "$TMP" - - name: Install PDFium (Windows) + - name: Deploy PDFium (Windows) if: matrix.platform == 'windows-latest' env: PDFIUM_VERSION: ${{ env.PDFIUM_VERSION }} @@ -464,7 +464,7 @@ jobs: echo "Cleaning up ..." rm -fr "$TMP" - - name: Install Qdrant (Windows) + - name: Deploy Qdrant (Windows) if: matrix.platform == 'windows-latest' env: QDRANT_VERSION: ${{ env.QDRANT_VERSION }} @@ -479,6 +479,11 @@ jobs: $DB_SOURCE = "qdrant.exe" $DB_TARGET = "qdrant.exe" } + "win-arm64" { + $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 diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs index 9a573823d..4133332eb 100644 --- a/app/Build/Commands/Qdrant.cs +++ b/app/Build/Commands/Qdrant.cs @@ -91,10 +91,11 @@ public static async Task InstallAsync(RID rid, string version) 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_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-musl"), 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), }; @@ -111,7 +112,7 @@ private static string GetQdrantDownloadUrl(RID rid, string version) 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 + RID.WIN_ARM64 => $"{baseUrl}x86_64-pc-windows-msvc.zip", _ => string.Empty, }; diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index b4b16cd2f..572c35043 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -52,6 +52,7 @@ + diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor index d9fd748b4..b442a8336 100644 --- a/app/MindWork AI Studio/Pages/About.razor +++ b/app/MindWork AI Studio/Pages/About.razor @@ -25,12 +25,12 @@ - @foreach (var (Label, Value) in DatabaseClient.GetDisplayInfo()) + @foreach (var (label, value) in DatabaseDisplayInfo) {
- @Label: @Value - + @label: @value +
}
diff --git a/app/MindWork AI Studio/Pages/About.razor.cs b/app/MindWork AI Studio/Pages/About.razor.cs index 9371c610d..5f3c2fce8 100644 --- a/app/MindWork AI Studio/Pages/About.razor.cs +++ b/app/MindWork AI Studio/Pages/About.razor.cs @@ -70,6 +70,8 @@ public partial class About : MSGComponentBase private bool showDatabaseDetails = false; private IPluginMetadata? configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION); + + private List<(string Label, string Value)> DatabaseDisplayInfo = new(); /// /// Determines whether the enterprise configuration has details that can be shown/hidden. @@ -105,6 +107,11 @@ protected override async Task OnInitializedAsync() this.osLanguage = await this.RustService.ReadUserLanguage(); this.logPaths = await this.RustService.GetLogPaths(); + await foreach (var item in this.DatabaseClient.GetDisplayInfo()) + { + this.DatabaseDisplayInfo.Add(item); + } + // Determine the Pandoc version may take some time, so we start it here // without waiting for the result: _ = this.DeterminePandocVersion(); diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 0b63f17a5..b67cdcfeb 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -27,6 +27,7 @@ internal sealed class Program public static string API_TOKEN = null!; public static IServiceProvider SERVICE_PROVIDER = null!; public static ILoggerFactory LOGGER_FACTORY = null!; + public static DatabaseClient DATABASE_CLIENT = null!; public static async Task Main() { @@ -102,6 +103,20 @@ public static async Task Main() Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust."); return; } + + if (qdrantInfo.Fingerprint == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust."); + return; + } + + if (qdrantInfo.ApiToken == string.Empty) + { + Console.WriteLine("Error: Failed to get the Qdrant API token from Rust."); + return; + } + + var databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken); var builder = WebApplication.CreateBuilder(); @@ -155,7 +170,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)); + builder.Services.AddSingleton(databaseClient); // ReSharper disable AccessToDisposedClosure builder.Services.AddHostedService(_ => rust); @@ -211,6 +226,10 @@ public static async Task Main() RUST_SERVICE = rust; ENCRYPTION = encryption; + + var databaseLogger = app.Services.GetRequiredService>(); + databaseClient.SetLogger(databaseLogger); + DATABASE_CLIENT = databaseClient; programLogger.LogInformation("Initialize internal file system."); app.Use(Redirect.HandlerContentAsync); @@ -238,7 +257,6 @@ 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"}'."); @@ -248,6 +266,7 @@ public static async Task Main() await serverTask; RUST_SERVICE.Dispose(); + DATABASE_CLIENT.Dispose(); PluginFactory.Dispose(); programLogger.LogInformation("The AI Studio server was stopped."); } diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs index 0ca84e013..3881a9fcf 100644 --- a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs @@ -1,57 +1,29 @@ namespace AIStudio.Tools.Databases; -public abstract class DatabaseClient +public abstract class DatabaseClient(string name, string path) { - public string Name { get; } - private string Path { get; } - - public DatabaseClient(string name, string path) - { - this.Name = name; - this.Path = path; - } + public string Name => name; + private string Path => path; + protected ILogger? logger; - public abstract IEnumerable<(string Label, string Value)> GetDisplayInfo(); + public abstract IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo(); public string GetStorageSize() { - if (string.IsNullOrEmpty(this.Path)) + if (string.IsNullOrWhiteSpace(this.Path)) { - Console.WriteLine($"Error: Database path '{this.Path}' cannot be null or empty."); + this.logger!.LogError($"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."); + this.logger!.LogError($"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}"); - } - } + var files = Directory.EnumerateFiles(this.Path, "*", SearchOption.AllDirectories) + .Where(file => !System.IO.Path.GetDirectoryName(file)!.Contains("cert", StringComparison.OrdinalIgnoreCase)); + var size = files.Sum(file => new FileInfo(file).Length); return FormatBytes(size); } @@ -68,4 +40,11 @@ public static string FormatBytes(long size) return $"{size:0##} {suffixes[suffixIndex]}"; } + + public void SetLogger(ILogger logService) + { + this.logger = logService; + } + + public abstract void Dispose(); } \ 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 deleted file mode 100644 index c3a4fabd3..000000000 --- a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClient.cs +++ /dev/null @@ -1,15 +0,0 @@ -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/Databases/Qdrant/QdrantClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs new file mode 100644 index 000000000..4ef49dc19 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs @@ -0,0 +1,61 @@ +using Qdrant.Client; +using Qdrant.Client.Grpc; + +namespace AIStudio.Tools.Databases.Qdrant; + +public class QdrantClientImplementation : DatabaseClient +{ + private int HttpPort { get; } + private int GrpcPort { get; } + private string IpAddress => "localhost"; + 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 QdrantClient CreateQdrantClient() + { + var address = "https://" + this.IpAddress + ":" + this.GrpcPort; + var channel = QdrantChannel.ForAddress(address, new ClientConfiguration + { + ApiKey = this.ApiToken, + CertificateThumbprint = this.Fingerprint + }); + var grpcClient = new QdrantGrpcClient(channel); + return new QdrantClient(grpcClient); + } + + public async Task GetVersion() + { + var operation = await this.GrpcClient.HealthAsync(); + return "v"+operation.Version; + } + + public async Task GetCollectionsAmount() + { + var operation = await this.GrpcClient.ListCollectionsAsync(); + return operation.Count.ToString(); + } + + public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo() + { + yield return ("HTTP port", this.HttpPort.ToString()); + yield return ("gRPC port", this.GrpcPort.ToString()); + yield return ("Extracted version", await this.GetVersion()); + yield return ("Storage size", $"{base.GetStorageSize()}"); + yield return ("Amount 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/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs index 8cbe5e9c8..6f9b2e5cd 100644 --- a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs +++ b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs @@ -10,4 +10,6 @@ public record struct QdrantInfo 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 ae42316dc..a4e0eade6 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs @@ -9,7 +9,7 @@ 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); + var response = await this.http.GetFromJsonAsync("/system/qdrant/info", this.jsonRustSerializerOptions, cts.Token); return response; } catch (Exception e) @@ -20,6 +20,8 @@ public async Task GetQdrantInfo() Path = string.Empty, PortHttp = 0, PortGrpc = 0, + Fingerprint = string.Empty, + ApiToken = string.Empty, }; } } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 3eb33086c..a518b32d2 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -39,6 +39,7 @@ pdfium-render = "0.8.34" sys-locale = "0.3.2" cfg-if = "1.0.1" pptx-to-md = "0.4.0" +tempfile = "3.8" # Fixes security vulnerability downstream, where the upstream is not fixed yet: url = "2.5" diff --git a/runtime/resources/databases/qdrant/config.yaml b/runtime/resources/databases/qdrant/config.yaml index 267f81c24..1149a0ece 100644 --- a/runtime/resources/databases/qdrant/config.yaml +++ b/runtime/resources/databases/qdrant/config.yaml @@ -332,10 +332,10 @@ telemetry_disabled: true # Required if either service.enable_tls or cluster.p2p.enable_tls is true. tls: # Server certificate chain file - cert: ./tls/cert.pem + # cert: ./tls/cert.pem # Server private key file - key: ./tls/key.pem + # key: ./tls/key.pem # Certificate authority certificate file. # This certificate will be used to validate the certificates diff --git a/runtime/src/api_token.rs b/runtime/src/api_token.rs index 317591854..e945095ea 100644 --- a/runtime/src/api_token.rs +++ b/runtime/src/api_token.rs @@ -1,21 +1,5 @@ -use log::info; -use once_cell::sync::Lazy; use rand::{RngCore, SeedableRng}; -use rocket::http::Status; -use rocket::Request; -use rocket::request::FromRequest; - -/// The API token used to authenticate requests. -pub static API_TOKEN: Lazy = Lazy::new(|| { - let mut token = [0u8; 32]; - let mut rng = rand_chacha::ChaChaRng::from_os_rng(); - rng.fill_bytes(&mut token); - - let token = APIToken::from_bytes(token.to_vec()); - info!("API token was generated successfully."); - - token -}); +use rand_chacha::ChaChaRng; /// The API token data structure used to authenticate requests. pub struct APIToken { @@ -34,7 +18,7 @@ impl APIToken { } /// Creates a new API token from a hexadecimal text. - fn from_hex_text(hex_text: &str) -> Self { + pub fn from_hex_text(hex_text: &str) -> Self { APIToken { hex_text: hex_text.to_string(), } @@ -45,40 +29,14 @@ impl APIToken { } /// Validates the received token against the valid token. - fn validate(&self, received_token: &Self) -> bool { + pub fn validate(&self, received_token: &Self) -> bool { received_token.to_hex_text() == self.to_hex_text() } } -/// The request outcome type used to handle API token requests. -type RequestOutcome = rocket::request::Outcome; - -/// The request outcome implementation for the API token. -#[rocket::async_trait] -impl<'r> FromRequest<'r> for APIToken { - type Error = APITokenError; - - /// Handles the API token requests. - async fn from_request(request: &'r Request<'_>) -> RequestOutcome { - let token = request.headers().get_one("token"); - match token { - Some(token) => { - let received_token = APIToken::from_hex_text(token); - if API_TOKEN.validate(&received_token) { - RequestOutcome::Success(received_token) - } else { - RequestOutcome::Error((Status::Unauthorized, APITokenError::Invalid)) - } - } - - None => RequestOutcome::Error((Status::Unauthorized, APITokenError::Missing)), - } - } -} - -/// The API token error types. -#[derive(Debug)] -pub enum APITokenError { - Missing, - Invalid, +pub fn generate_api_token() -> APIToken { + let mut token = [0u8; 32]; + let mut rng = ChaChaRng::from_os_rng(); + rng.fill_bytes(&mut token); + APIToken::from_bytes(token.to_vec()) } \ No newline at end of file diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 7cd97b8bc..696615533 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -17,7 +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; +use crate::qdrant::{start_qdrant_server, stop_qdrant_server}; /// The Tauri main window. static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)); @@ -174,6 +174,7 @@ pub fn start_tauri() { RunEvent::ExitRequested { .. } => { warn!(Source = "Tauri"; "Run event: exit was requested."); + stop_qdrant_server(); } RunEvent::Ready => { diff --git a/runtime/src/certificate.rs b/runtime/src/certificate.rs deleted file mode 100644 index 8cf7fb38a..000000000 --- a/runtime/src/certificate.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::sync::OnceLock; -use log::info; -use rcgen::generate_simple_self_signed; -use sha2::{Sha256, Digest}; - -/// The certificate used for the runtime API server. -pub static CERTIFICATE: OnceLock> = OnceLock::new(); - -/// The private key used for the certificate of the runtime API server. -pub static CERTIFICATE_PRIVATE_KEY: OnceLock> = OnceLock::new(); - -/// The fingerprint of the certificate used for the runtime API server. -pub static CERTIFICATE_FINGERPRINT: OnceLock = OnceLock::new(); - -/// Generates a TLS certificate for the runtime API server. -pub fn generate_certificate() { - - info!("Try to generate a TLS certificate for the runtime API server..."); - - let subject_alt_names = vec!["localhost".to_string()]; - let certificate_data = generate_simple_self_signed(subject_alt_names).unwrap(); - let certificate_binary_data = certificate_data.cert.der().to_vec(); - - let certificate_fingerprint = Sha256::digest(certificate_binary_data).to_vec(); - let certificate_fingerprint = certificate_fingerprint.iter().fold(String::new(), |mut result, byte| { - result.push_str(&format!("{:02x}", byte)); - result - }); - - let certificate_fingerprint = certificate_fingerprint.to_uppercase(); - - CERTIFICATE_FINGERPRINT.set(certificate_fingerprint.clone()).expect("Could not set the certificate fingerprint."); - CERTIFICATE.set(certificate_data.cert.pem().as_bytes().to_vec()).expect("Could not set the certificate."); - CERTIFICATE_PRIVATE_KEY.set(certificate_data.signing_key.serialize_pem().as_bytes().to_vec()).expect("Could not set the private key."); - - info!("Certificate fingerprint: '{certificate_fingerprint}'."); - info!("Done generating certificate for the runtime API server."); -} \ No newline at end of file diff --git a/runtime/src/certificate_factory.rs b/runtime/src/certificate_factory.rs new file mode 100644 index 000000000..3c30d34ab --- /dev/null +++ b/runtime/src/certificate_factory.rs @@ -0,0 +1,22 @@ +use log::info; +use rcgen::generate_simple_self_signed; +use sha2::{Sha256, Digest}; + +pub fn generate_certificate() -> (Vec, Vec, String) { + + let subject_alt_names = vec!["localhost".to_string()]; + let certificate_data = generate_simple_self_signed(subject_alt_names).unwrap(); + let certificate_binary_data = certificate_data.cert.der().to_vec(); + + let certificate_fingerprint = Sha256::digest(certificate_binary_data).to_vec(); + let certificate_fingerprint = certificate_fingerprint.iter().fold(String::new(), |mut result, byte| { + result.push_str(&format!("{:02x}", byte)); + result + }); + + let certificate_fingerprint = certificate_fingerprint.to_uppercase(); + + info!("Certificate fingerprint: '{certificate_fingerprint}'."); + + (certificate_data.cert.pem().as_bytes().to_vec(), certificate_data.signing_key.serialize_pem().as_bytes().to_vec(), certificate_fingerprint.clone()) +} \ No newline at end of file diff --git a/runtime/src/dotnet.rs b/runtime/src/dotnet.rs index 26b793f5d..fb792a156 100644 --- a/runtime/src/dotnet.rs +++ b/runtime/src/dotnet.rs @@ -7,9 +7,10 @@ use once_cell::sync::Lazy; use rocket::get; use tauri::api::process::{Command, CommandChild, CommandEvent}; use tauri::Url; -use crate::api_token::{APIToken, API_TOKEN}; +use crate::api_token::APIToken; +use crate::runtime_api_token::API_TOKEN; use crate::app_window::change_location_to; -use crate::certificate::CERTIFICATE_FINGERPRINT; +use crate::runtime_certificate::CERTIFICATE_FINGERPRINT; use crate::encryption::ENCRYPTION; use crate::environment::is_dev; use crate::network::get_available_port; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index bd7da3078..e99f528bd 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -8,9 +8,11 @@ pub mod app_window; pub mod secret; pub mod clipboard; pub mod runtime_api; -pub mod certificate; +pub mod runtime_certificate; pub mod file_data; pub mod metadata; pub mod pdfium; pub mod pandoc; -pub mod qdrant; \ No newline at end of file +pub mod qdrant; +pub mod certificate_factory; +pub mod runtime_api_token; \ No newline at end of file diff --git a/runtime/src/main.rs b/runtime/src/main.rs index bfbe4750c..91427472c 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -6,7 +6,7 @@ extern crate core; use log::{info, warn}; use mindwork_ai_studio::app_window::start_tauri; -use mindwork_ai_studio::certificate::{generate_certificate}; +use mindwork_ai_studio::runtime_certificate::{generate_runtime_certificate}; use mindwork_ai_studio::dotnet::start_dotnet_server; use mindwork_ai_studio::environment::is_dev; use mindwork_ai_studio::log::init_logging; @@ -46,7 +46,7 @@ async fn main() { info!("Running in production mode."); } - generate_certificate(); + generate_runtime_certificate(); start_runtime_api(); if is_dev() { diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 3b2b94ced..4a945d5d3 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; +use std::fs::File; +use std::io::Write; use std::path::Path; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use log::{debug, error, info, warn}; use once_cell::sync::Lazy; use rocket::get; @@ -9,6 +11,9 @@ use rocket::serde::Serialize; use tauri::api::process::{Command, CommandChild, CommandEvent}; use crate::api_token::{APIToken}; use crate::environment::DATA_DIRECTORY; +use crate::certificate_factory::generate_certificate; +use std::path::PathBuf; +use tempfile::{TempDir, Builder}; // Qdrant server process started in a separate process and can communicate // via HTTP or gRPC with the .NET server and the runtime process @@ -23,26 +28,38 @@ 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)); + #[derive(Serialize)] pub struct ProvideQdrantInfo { path: String, port_http: u16, port_grpc: u16, + fingerprint: String, + api_token: String, } -#[get("/system/qdrant/port")] +#[get("/system/qdrant/info")] 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, + fingerprint: CERTIFICATE_FINGERPRINT.get().expect("Certificate fingerprint not available").to_string(), + api_token: API_TOKEN.to_hex_text().to_string(), }); } /// Starts the Qdrant server in a separate process. pub fn start_qdrant_server() { - let base_path = DATA_DIRECTORY.get().unwrap(); + let base_path = DATA_DIRECTORY.get().unwrap(); + let (cert_path, key_path) =create_temp_tls_files(Path::new(base_path).join("databases").join("qdrant")).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(); @@ -54,6 +71,10 @@ pub fn start_qdrant_server() { (String::from("QDRANT_INIT_FILE_PATH"), init_path), (String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path), (String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path), + (String::from("QDRANT__TLS__CERT"), cert_path.to_str().unwrap().to_string()), + (String::from("QDRANT__TLS__KEY"), key_path.to_str().unwrap().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(); @@ -97,13 +118,51 @@ pub fn start_qdrant_server() { /// Stops the Qdrant server process. pub fn stop_qdrant_server() { + drop_tmpdir(); 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}."), + Ok(_) => warn!(Source = "Qdrant"; "Qdrant server process was stopped."), + Err(e) => error!(Source = "Qdrant"; "Failed to stop Qdrant server process: {e}."), } } else { - warn!("Qdrant server process was not started or is already stopped."); + warn!(Source = "Qdrant"; "Qdrant server process was not started or is already stopped."); } +} + +pub fn create_temp_tls_files(path: PathBuf) -> Result<(PathBuf, PathBuf), Box> { + let (certificate, cert_private_key, cert_fingerprint) = 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(&*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."); } \ No newline at end of file diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 529d96369..d08c5abe7 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -3,7 +3,7 @@ use once_cell::sync::Lazy; use rocket::config::Shutdown; use rocket::figment::Figment; use rocket::routes; -use crate::certificate::{CERTIFICATE, CERTIFICATE_PRIVATE_KEY}; +use crate::runtime_certificate::{CERTIFICATE, CERTIFICATE_PRIVATE_KEY}; use crate::environment::is_dev; use crate::network::get_available_port; diff --git a/runtime/src/runtime_api_token.rs b/runtime/src/runtime_api_token.rs new file mode 100644 index 000000000..f1e762c9a --- /dev/null +++ b/runtime/src/runtime_api_token.rs @@ -0,0 +1,40 @@ +use once_cell::sync::Lazy; +use rocket::http::Status; +use rocket::Request; +use rocket::request::FromRequest; +use crate::api_token::{generate_api_token, APIToken}; + +pub static API_TOKEN: Lazy = Lazy::new(|| generate_api_token()); + +/// The request outcome type used to handle API token requests. +type RequestOutcome = rocket::request::Outcome; + +/// The request outcome implementation for the API token. +#[rocket::async_trait] +impl<'r> FromRequest<'r> for APIToken { + type Error = APITokenError; + + /// Handles the API token requests. + async fn from_request(request: &'r Request<'_>) -> RequestOutcome { + let token = request.headers().get_one("token"); + match token { + Some(token) => { + let received_token = APIToken::from_hex_text(token); + if API_TOKEN.validate(&received_token) { + RequestOutcome::Success(received_token) + } else { + RequestOutcome::Error((Status::Unauthorized, APITokenError::Invalid)) + } + } + + None => RequestOutcome::Error((Status::Unauthorized, APITokenError::Missing)), + } + } +} + +/// The API token error types. +#[derive(Debug)] +pub enum APITokenError { + Missing, + Invalid, +} \ No newline at end of file diff --git a/runtime/src/runtime_certificate.rs b/runtime/src/runtime_certificate.rs new file mode 100644 index 000000000..abbde65cb --- /dev/null +++ b/runtime/src/runtime_certificate.rs @@ -0,0 +1,26 @@ +use std::sync::OnceLock; +use log::info; +use crate::certificate_factory::generate_certificate; + +/// The certificate used for the runtime API server. +pub static CERTIFICATE: OnceLock> = OnceLock::new(); + +/// The private key used for the certificate of the runtime API server. +pub static CERTIFICATE_PRIVATE_KEY: OnceLock> = OnceLock::new(); + +/// The fingerprint of the certificate used for the runtime API server. +pub static CERTIFICATE_FINGERPRINT: OnceLock = OnceLock::new(); + +/// Generates a TLS certificate for the runtime API server. +pub fn generate_runtime_certificate() { + + info!("Try to generate a TLS certificate for the runtime API server..."); + + let (certificate, cer_private_key, cer_fingerprint) = generate_certificate(); + + CERTIFICATE_FINGERPRINT.set(cer_fingerprint).expect("Could not set the certificate fingerprint."); + CERTIFICATE.set(certificate).expect("Could not set the certificate."); + CERTIFICATE_PRIVATE_KEY.set(cer_private_key).expect("Could not set the private key."); + + info!("Done generating certificate for the runtime API server."); +} \ No newline at end of file From 3f7b230e6b26c8812724af4dd125df8a3efe8e16 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 14 Jan 2026 09:01:16 +0100 Subject: [PATCH 04/30] Formatting --- app/Build/Commands/Qdrant.cs | 5 +++-- app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs | 2 ++ .../Tools/Databases/Qdrant/QdrantClientImplementation.cs | 3 +++ app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs | 4 ++++ runtime/src/qdrant.rs | 8 ++++---- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs index 4133332eb..f51c0fa78 100644 --- a/app/Build/Commands/Qdrant.cs +++ b/app/Build/Commands/Qdrant.cs @@ -44,7 +44,8 @@ public static async Task InstallAsync(RID rid, string version) { using var archive = new ZipArchive(zStream, ZipArchiveMode.Read); archive.ExtractToDirectory(qdrantTmpExtractPath.FullName, overwriteFiles: true); - } else + } + else { await using var uncompressedStream = new GZipStream(zStream, CompressionMode.Decompress); await TarFile.ExtractToDirectoryAsync(uncompressedStream, qdrantTmpExtractPath.FullName, true); @@ -63,7 +64,7 @@ public static async Task InstallAsync(RID rid, string version) } var qdrantDBSourcePath = Path.Join(qdrantTmpExtractPath.FullName, database.Path); - var qdrantDBTargetPath = Path.Join(cwd, "resources", "databases", "qdrant",database.Filename); + var qdrantDBTargetPath = Path.Join(cwd, "resources", "databases", "qdrant", database.Filename); if (!File.Exists(qdrantDBSourcePath)) { Console.WriteLine($" failed to find the database file '{qdrantDBSourcePath}'"); diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs index 3881a9fcf..5ea457ec4 100644 --- a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs @@ -3,7 +3,9 @@ public abstract class DatabaseClient(string name, string path) { public string Name => name; + private string Path => path; + protected ILogger? logger; public abstract IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo(); diff --git a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs index 4ef49dc19..e4cd5a2c9 100644 --- a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs +++ b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs @@ -6,10 +6,13 @@ namespace AIStudio.Tools.Databases.Qdrant; public class QdrantClientImplementation : DatabaseClient { private int HttpPort { get; } + private int GrpcPort { get; } private string IpAddress => "localhost"; 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) diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs index 6f9b2e5cd..8dac8aa76 100644 --- a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs +++ b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs @@ -8,8 +8,12 @@ public record struct QdrantInfo { 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/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 4a945d5d3..0ae5a2ee7 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -106,10 +106,12 @@ pub fn start_qdrant_server() { } else { debug!(Source = "Qdrant Server"; "{line}"); } - } + }, + CommandEvent::Stderr(line) => { error!(Source = "Qdrant Server (stderr)"; "{line}"); - } + }, + _ => {} } } @@ -132,9 +134,7 @@ pub fn stop_qdrant_server() { pub fn create_temp_tls_files(path: PathBuf) -> Result<(PathBuf, PathBuf), Box> { let (certificate, cert_private_key, cert_fingerprint) = 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"); From ef3d17de0fbce02292b301968bc871080931ac83 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 14 Jan 2026 09:01:51 +0100 Subject: [PATCH 05/30] Use a const instead --- .../Tools/Databases/Qdrant/QdrantClientImplementation.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs index e4cd5a2c9..77ae36365 100644 --- a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs +++ b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs @@ -8,7 +8,7 @@ public class QdrantClientImplementation : DatabaseClient private int HttpPort { get; } private int GrpcPort { get; } - private string IpAddress => "localhost"; + private QdrantClient GrpcClient { get; } private string Fingerprint { get; } @@ -24,9 +24,11 @@ public QdrantClientImplementation(string name, string path, int httpPort, int gr this.GrpcClient = this.CreateQdrantClient(); } + private const string IP_ADDRESS = "localhost"; + public QdrantClient CreateQdrantClient() { - var address = "https://" + this.IpAddress + ":" + this.GrpcPort; + var address = "https://" + IP_ADDRESS + ":" + this.GrpcPort; var channel = QdrantChannel.ForAddress(address, new ClientConfiguration { ApiKey = this.ApiToken, From 42d538d2d8881b977ba223531ba32e64b294aad4 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 14 Jan 2026 09:02:14 +0100 Subject: [PATCH 06/30] Removed not used documentation --- app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs index 8dac8aa76..4b22837d1 100644 --- a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs +++ b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs @@ -3,8 +3,6 @@ /// /// 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; } From cb453dc51802a30e2382bff534342f4fb6158d02 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 14 Jan 2026 09:03:20 +0100 Subject: [PATCH 07/30] Use a readonly record struct --- app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs | 2 +- .../Tools/Services/RustService.Databases.cs | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs index 4b22837d1..c847235f8 100644 --- a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs +++ b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs @@ -3,7 +3,7 @@ /// /// The response of the Qdrant information request. /// -public record struct QdrantInfo +public readonly record struct QdrantInfo { public string Path { get; init; } diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs index a4e0eade6..8f67a1d4a 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs @@ -15,14 +15,7 @@ public async Task GetQdrantInfo() catch (Exception e) { Console.WriteLine(e); - return new QdrantInfo - { - Path = string.Empty, - PortHttp = 0, - PortGrpc = 0, - Fingerprint = string.Empty, - ApiToken = string.Empty, - }; + return default; } } } \ No newline at end of file From d9bec277639d817c079ae8381003ec86bd01908b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 14 Jan 2026 09:03:32 +0100 Subject: [PATCH 08/30] Improved logging --- .../Tools/Services/RustService.Databases.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs index 8f67a1d4a..a43f6c611 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs @@ -14,7 +14,11 @@ public async Task GetQdrantInfo() } catch (Exception e) { - Console.WriteLine(e); + if(this.logger is not null) + this.logger.LogError(e, "Error while fetching Qdrant info from Rust service."); + else + Console.WriteLine($"Error while fetching Qdrant info from Rust service: '{e}'."); + return default; } } From e92b7be5df961a3df9154e2b0c889cdba14e6800 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 14 Jan 2026 09:03:45 +0100 Subject: [PATCH 09/30] Fixed C# syntax --- app/MindWork AI Studio/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index b67cdcfeb..4c495bb34 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -87,7 +87,7 @@ public static async Task Main() } var qdrantInfo = await rust.GetQdrantInfo(); - if (qdrantInfo.Path == String.Empty) + if (qdrantInfo.Path == string.Empty) { Console.WriteLine("Error: Failed to get the Qdrant path from Rust."); return; From e383072ba6e819724f77b8594d4ed652ac96e5c6 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 14 Jan 2026 09:03:58 +0100 Subject: [PATCH 10/30] Added Qdrant to dict --- app/MindWork AI Studio.sln.DotSettings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/MindWork AI Studio.sln.DotSettings b/app/MindWork AI Studio.sln.DotSettings index faaedb6b5..a304dae68 100644 --- a/app/MindWork AI Studio.sln.DotSettings +++ b/app/MindWork AI Studio.sln.DotSettings @@ -24,4 +24,6 @@ True True True + True + True True \ No newline at end of file From a4e0ee29609d6f920c3433a7ea1d9732c2556a34 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 14 Jan 2026 09:04:12 +0100 Subject: [PATCH 11/30] Formatting --- runtime/src/qdrant.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 0ae5a2ee7..dc0ea1533 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -59,7 +59,7 @@ pub fn qdrant_port(_token: APIToken) -> Json { pub fn start_qdrant_server() { let base_path = DATA_DIRECTORY.get().unwrap(); - let (cert_path, key_path) =create_temp_tls_files(Path::new(base_path).join("databases").join("qdrant")).unwrap(); + let (cert_path, key_path) = create_temp_tls_files(Path::new(base_path).join("databases").join("qdrant")).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(); From f57973148f51eae8983748872170e01fb780f64b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 14 Jan 2026 09:04:32 +0100 Subject: [PATCH 12/30] Drop the temp. directory last --- runtime/src/qdrant.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index dc0ea1533..c3f0fa1e7 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -120,7 +120,6 @@ pub fn start_qdrant_server() { /// Stops the Qdrant server process. pub fn stop_qdrant_server() { - drop_tmpdir(); if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() { let server_kill_result = server_process.kill(); match server_kill_result { @@ -130,6 +129,8 @@ pub fn stop_qdrant_server() { } else { warn!(Source = "Qdrant"; "Qdrant server process was not started or is already stopped."); } + + drop_tmpdir(); } pub fn create_temp_tls_files(path: PathBuf) -> Result<(PathBuf, PathBuf), Box> { From 1a0a6e256bfed0d46dbd17f2f489d6f5e870d759 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 14 Jan 2026 09:04:48 +0100 Subject: [PATCH 13/30] Updated packages --- app/MindWork AI Studio/packages.lock.json | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index c36716e7c..4d61757d2 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -64,6 +64,16 @@ "MudBlazor": "8.11.0" } }, + "Qdrant.Client": { + "type": "Direct", + "requested": "[1.16.1, )", + "resolved": "1.16.1", + "contentHash": "EJo50JXTdjY2JOUphCFLXoHukI/tz/ykLCmMnQHUjsKT22ZfL0XIdEziHOC3vjw2SOoY8WDVQ+AxixEonejOZA==", + "dependencies": { + "Google.Protobuf": "3.31.0", + "Grpc.Net.Client": "2.71.0" + } + }, "ReverseMarkdown": { "type": "Direct", "requested": "[4.7.1, )", @@ -83,6 +93,33 @@ "resolved": "33.0.1", "contentHash": "fev4lynklAU2A9GVMLtwarkwaanjSYB4wUqO2nOJX5hnzObORzUqVLe+bDYCUyIIRQM4o5Bsq3CcyJR89iMmEQ==" }, + "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" + } + }, "Markdig": { "type": "Transitive", "resolved": "0.41.3", From 8d850d88fb61bd8e4d9f96c1e089bed5515e81ed Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Fri, 16 Jan 2026 20:48:09 +0100 Subject: [PATCH 14/30] Added handling of zombie processes in case of crashes and streamlined API and certificate generation. --- app/Build/Commands/Qdrant.cs | 18 +- runtime/Cargo.toml | 2 + runtime/build.rs | 198 +++++++++++++++++- .../resources/databases/qdrant/config.yaml | 5 +- runtime/src/app_window.rs | 19 +- runtime/src/certificate_factory.rs | 14 +- runtime/src/dotnet.rs | 18 +- runtime/src/lib.rs | 3 +- runtime/src/qdrant.rs | 74 +++++-- runtime/src/runtime_certificate.rs | 8 +- runtime/src/zombie_process_remover.rs | 145 +++++++++++++ runtime/tauri.conf.json | 4 +- 12 files changed, 466 insertions(+), 42 deletions(-) create mode 100644 runtime/src/zombie_process_remover.rs diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs index 4133332eb..923b5a47e 100644 --- a/app/Build/Commands/Qdrant.cs +++ b/app/Build/Commands/Qdrant.cs @@ -62,19 +62,19 @@ public static async Task InstallAsync(RID rid, string version) return; } - var qdrantDBSourcePath = Path.Join(qdrantTmpExtractPath.FullName, database.Path); - var qdrantDBTargetPath = Path.Join(cwd, "resources", "databases", "qdrant",database.Filename); - if (!File.Exists(qdrantDBSourcePath)) + 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}'"); + 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); + Directory.CreateDirectory(Path.Join(cwd, "target", "databases", "qdrant")); + if (File.Exists(qdrantDbTargetPath)) + File.Delete(qdrantDbTargetPath); - File.Copy(qdrantDBSourcePath, qdrantDBTargetPath); + File.Copy(qdrantDbSourcePath, qdrantDbTargetPath); // // Cleanup: @@ -91,7 +91,7 @@ public static async Task InstallAsync(RID rid, string version) 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-musl"), + 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"), diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a518b32d2..bb28ee6e1 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -7,6 +7,7 @@ authors = ["Thorsten Sommer"] [build-dependencies] tauri-build = { version = "1.5", features = [] } +dirs = "6.0.0" [dependencies] tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog"] } @@ -46,6 +47,7 @@ url = "2.5" ring = "0.17.14" crossbeam-channel = "0.5.15" tracing-subscriber = "0.3.20" +dirs = "6.0.0" [target.'cfg(target_os = "linux")'.dependencies] # See issue https://github.com/tauri-apps/tauri/issues/4470 diff --git a/runtime/build.rs b/runtime/build.rs index 93871d1ad..afa013cf1 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -1,6 +1,39 @@ -use std::path::PathBuf; +use std::{env, fs}; +use std::path::{PathBuf}; +use std::process::Command; +use std::io::{Error, ErrorKind}; fn main() { + match env::var("MINDWORK_START_DEV_ENV") { + Ok(val) => { + let is_started_manually = match val.parse::() { + Ok(b) => b, + Err(_) => { + println!("cargo: warning= Invalid value for MINDWORK_START_DEV_ENV: expected 'true' or 'false'"); + return; + } + }; + if is_started_manually { + if let Err(e) = kill_zombie_qdrant_process(){ + println!("cargo:warning=Error: {e}"); + return; + }; + if let Err(e) = delete_old_certificates() { + println!("cargo: warning= Failed to delete old certificates: {e}"); + } + } + }, + Err(_) => { + println!("cargo: warning= The environment variable 'MINDWORK_START_DEV_ENV' was not found."); + if let Err(e) = kill_zombie_qdrant_process(){ + println!("cargo:warning=Error: {e}"); + return; + }; + if let Err(e) = delete_old_certificates() { + println!("cargo: warning= Failed to delete old certificates: {e}"); + } + } + } tauri_build::build(); let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); @@ -84,3 +117,166 @@ fn update_tauri_conf(tauri_conf_path: &str, version: &str) { std::fs::write(tauri_conf_path, new_tauri_conf).unwrap(); } + +#[cfg(unix)] +pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { + // + // Check if PID exists and name matches + // + let ps_output = Command::new("ps") + .arg("-p") + .arg(pid.to_string()) + .arg("-o") + .arg("comm=") + .output()?; + + let output = String::from_utf8_lossy(&ps_output.stdout).trim().to_string(); + + if output.is_empty() { + // Process doesn't exist + return Ok(()); + } + + let name = output; + if name != expected_name { + return Err(Error::new(ErrorKind::InvalidInput, "Process name does not match")); + } + + // + // Kill the process + // + let kill_output = Command::new("kill") + .arg("-9") + .arg(pid.to_string()) + .output()?; + + if !kill_output.status.success() { + return Err(Error::new(ErrorKind::Other, "Failed to kill process")); + } + + // + // Verify process is killed + // + let ps_check = Command::new("ps") + .arg("-p") + .arg(pid.to_string()) + .output()?; + + let output = String::from_utf8_lossy(&ps_check.stdout).trim().to_string(); + if output.is_empty() { + Ok(()) + } else { + Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) + } +} + +#[cfg(windows)] +pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { + // + // Check if PID exists and name matches + // + let tasklist_output = Command::new("tasklist") + .arg("/FI") + .arg(format!("PID eq {}", pid)) + .arg("/FO") + .arg("CSV") + .arg("/NH") + .output()?; + + let output = String::from_utf8_lossy(&tasklist_output.stdout).trim().to_string(); + + if output.is_empty() || !output.starts_with('"') { + println!("cargo:warning= Pid file was found, but process was not."); + return Ok(()) + } + + let name = output.split(',').next().unwrap_or("").trim_matches('"'); + if name != expected_name { + return Err(Error::new(ErrorKind::InvalidInput, format!("Process name does not match. Expected:{}, got:{}",expected_name,name))); + } + + // + // Kill the process + // + let kill_output = Command::new("taskkill") + .arg("/PID") + .arg(pid.to_string()) + .arg("/F") + .arg("/T") + .output()?; + + if !kill_output.status.success() { + return Err(Error::new(ErrorKind::Other, "Failed to kill process")); + } + + // + // Verify process is killed + // + let tasklist_check = Command::new("tasklist") + .arg("/FI") + .arg(format!("PID eq {}", pid)) + .output()?; + + let output = String::from_utf8_lossy(&tasklist_check.stdout).trim().to_string(); + if output.is_empty() || !output.starts_with('"') { + Ok(()) + } + else { + Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) + } +} + + +pub fn kill_zombie_qdrant_process() -> Result<(), Error> { + let pid_file = dirs::data_local_dir() + .expect("Local appdata was not found") + .join("com.github.mindwork-ai.ai-studio") + .join("data") + .join("databases") + .join("qdrant") + .join("qdrant.pid"); + + if !pid_file.exists() { + return Ok(()); + } + + let pid_str = fs::read_to_string(&pid_file)?; + let pid: u32 = pid_str.trim().parse().map_err(|_| {Error::new(ErrorKind::InvalidData, "Invalid PID in file")})?; + if let Err(e) = ensure_process_killed(pid, "qdrant.exe".as_ref()){ + return Err(e); + } + + fs::remove_file(&pid_file)?; + println!("cargo:warning= Killed qdrant process and deleted redundant Pid file: {}", pid_file.display()); + + Ok(()) +} + +pub fn delete_old_certificates() -> Result<(), Box> { + let dir_path = dirs::data_local_dir() + .expect("Local appdata was not found") + .join("com.github.mindwork-ai.ai-studio") + .join("data") + .join("databases") + .join("qdrant"); + + if !dir_path.exists() { + return Ok(()); + } + + for entry in fs::read_dir(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)?; + println!("cargo: warning= Removed old certificates in: {}", path.display()); + } + } + } + Ok(()) +} diff --git a/runtime/resources/databases/qdrant/config.yaml b/runtime/resources/databases/qdrant/config.yaml index 1149a0ece..50f03e084 100644 --- a/runtime/resources/databases/qdrant/config.yaml +++ b/runtime/resources/databases/qdrant/config.yaml @@ -249,10 +249,11 @@ service: # 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_cors: false # Enable HTTPS for the REST and gRPC API - enable_tls: false + # 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 diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 696615533..dbb521b67 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::sync::Mutex; use std::time::Duration; use log::{debug, error, info, trace, warn}; @@ -8,16 +9,16 @@ use rocket::serde::json::Json; use rocket::serde::Serialize; use serde::Deserialize; use tauri::updater::UpdateResponse; -use tauri::{FileDropEvent, UpdaterEvent, RunEvent, Manager, PathResolver, Window, WindowEvent}; +use tauri::{FileDropEvent, UpdaterEvent, RunEvent, Manager, PathResolver, Window, WindowEvent, generate_context}; use tauri::api::dialog::blocking::FileDialogBuilder; use tokio::sync::broadcast; use tokio::time; use crate::api_token::APIToken; -use crate::dotnet::stop_dotnet_server; +use crate::dotnet::{cleanup_dotnet_server, stop_dotnet_server, PID_FILE_NAME}; 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::{cleanup_qdrant, start_qdrant_server, stop_qdrant_server}; /// The Tauri main window. static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)); @@ -92,16 +93,21 @@ pub fn start_tauri() { DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not abe to set the data directory.")).unwrap(); CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap(); + if is_prod() { + cleanup_qdrant().expect("Zombie processes of Qdrant were not killed"); + cleanup_dotnet_server(); + } + 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()) - .build(tauri::generate_context!()) + .build(generate_context!()) .expect("Error while running Tauri application"); // The app event handler: @@ -438,6 +444,7 @@ pub async fn install_update(_token: APIToken) { let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone(); match cloned_response_option { Some(update_response) => { + stop_qdrant_server(); update_response.download_and_install().await.unwrap(); }, diff --git a/runtime/src/certificate_factory.rs b/runtime/src/certificate_factory.rs index 3c30d34ab..c7dad76e8 100644 --- a/runtime/src/certificate_factory.rs +++ b/runtime/src/certificate_factory.rs @@ -2,7 +2,13 @@ use log::info; use rcgen::generate_simple_self_signed; use sha2::{Sha256, Digest}; -pub fn generate_certificate() -> (Vec, Vec, String) { +pub struct Certificate { + pub certificate: Vec, + pub private_key: Vec, + pub fingerprint: String, +} + +pub fn generate_certificate() -> Certificate { let subject_alt_names = vec!["localhost".to_string()]; let certificate_data = generate_simple_self_signed(subject_alt_names).unwrap(); @@ -18,5 +24,9 @@ pub fn generate_certificate() -> (Vec, Vec, String) { info!("Certificate fingerprint: '{certificate_fingerprint}'."); - (certificate_data.cert.pem().as_bytes().to_vec(), certificate_data.signing_key.serialize_pem().as_bytes().to_vec(), certificate_fingerprint.clone()) + Certificate { + certificate: certificate_data.cert.pem().as_bytes().to_vec(), + private_key: certificate_data.signing_key.serialize_pem().as_bytes().to_vec(), + fingerprint: certificate_fingerprint.clone() + } } \ No newline at end of file diff --git a/runtime/src/dotnet.rs b/runtime/src/dotnet.rs index fb792a156..7d2bc9c8f 100644 --- a/runtime/src/dotnet.rs +++ b/runtime/src/dotnet.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::path::Path; use std::sync::{Arc, Mutex}; use base64::Engine; use base64::prelude::BASE64_STANDARD; @@ -12,9 +13,10 @@ use crate::runtime_api_token::API_TOKEN; use crate::app_window::change_location_to; use crate::runtime_certificate::CERTIFICATE_FINGERPRINT; use crate::encryption::ENCRYPTION; -use crate::environment::is_dev; +use crate::environment::{is_dev, DATA_DIRECTORY}; use crate::network::get_available_port; use crate::runtime_api::API_SERVER_PORT; +use crate::zombie_process_remover::{kill_zombie_process, log_potential_zombie_process}; // The .NET server is started in a separate process and communicates with this // runtime process via IPC. However, we do net start the .NET server in @@ -27,6 +29,8 @@ static DOTNET_SERVER_PORT: Lazy = Lazy::new(|| get_available_port().unwrap( static DOTNET_INITIALIZED: Lazy> = Lazy::new(|| Mutex::new(false)); +pub const PID_FILE_NAME: &str = "mindwork_ai_studio.pid"; + /// Returns the desired port of the .NET server. Our .NET app calls this endpoint to get /// the port where the .NET server should listen to. #[get("/system/dotnet/port")] @@ -94,9 +98,9 @@ pub fn start_dotnet_server() { .envs(dotnet_server_environment) .spawn() .expect("Failed to spawn .NET server process."); - let server_pid = child.pid(); info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_pid}."); + log_potential_zombie_process(Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME), server_pid.to_string().as_str()); // Save the server process to stop it later: *server_spawn_clone.lock().unwrap() = Some(child); @@ -141,6 +145,7 @@ pub fn start_dotnet_server() { } } }); + } /// This endpoint is called by the .NET server to signal that the server is ready. @@ -185,4 +190,13 @@ pub fn stop_dotnet_server() { } else { warn!("The .NET server process was not started or is already stopped."); } + cleanup_dotnet_server(); +} + +/// Remove old Pid files and kill the corresponding processes +pub fn cleanup_dotnet_server() { + let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME); + if let Err(e) = kill_zombie_process(pid_path, "mindworkAIStudioServer.exe"){ + warn!(Source = ".NET"; "Error during the cleanup of .NET: {}", e); + } } \ No newline at end of file diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index e99f528bd..65b6abdf8 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -15,4 +15,5 @@ pub mod pdfium; pub mod pandoc; pub mod qdrant; pub mod certificate_factory; -pub mod runtime_api_token; \ No newline at end of file +pub mod runtime_api_token; +pub mod zombie_process_remover; \ No newline at end of file diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 4a945d5d3..3dac0e8c2 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -1,4 +1,6 @@ use std::collections::HashMap; +use std::{fs}; +use std::error::Error; use std::fs::File; use std::io::Write; use std::path::Path; @@ -14,6 +16,7 @@ use crate::environment::DATA_DIRECTORY; use crate::certificate_factory::generate_certificate; use std::path::PathBuf; use tempfile::{TempDir, Builder}; +use crate::zombie_process_remover::{kill_zombie_process, log_potential_zombie_process}; // Qdrant server process started in a separate process and can communicate // via HTTP or gRPC with the .NET server and the runtime process @@ -35,6 +38,8 @@ static API_TOKEN: Lazy = Lazy::new(|| { static TMPDIR: Lazy>> = Lazy::new(|| Mutex::new(None)); +const PID_FILE_NAME: &str = "qdrant.pid"; + #[derive(Serialize)] pub struct ProvideQdrantInfo { path: String, @@ -46,24 +51,30 @@ pub struct ProvideQdrantInfo { #[get("/system/qdrant/info")] pub fn qdrant_port(_token: APIToken) -> Json { - return Json(ProvideQdrantInfo { + 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, fingerprint: CERTIFICATE_FINGERPRINT.get().expect("Certificate fingerprint not available").to_string(), api_token: API_TOKEN.to_hex_text().to_string(), - }); + }) } /// Starts the Qdrant server in a separate process. -pub fn start_qdrant_server() { +pub fn start_qdrant_server(){ let base_path = DATA_DIRECTORY.get().unwrap(); - let (cert_path, key_path) =create_temp_tls_files(Path::new(base_path).join("databases").join("qdrant")).unwrap(); + let path = Path::new(base_path).join("databases").join("qdrant"); + if !path.exists() { + if let Err(e) = fs::create_dir_all(&path){ + error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e.to_string()); + }; + } + let (cert_path, key_path) =create_temp_tls_files(&path).unwrap(); - let storage_path = Path::new(base_path).join("databases").join("qdrant").join("storage").to_str().unwrap().to_string(); - let snapshot_path = Path::new(base_path).join("databases").join("qdrant").join("snapshots").to_str().unwrap().to_string(); - let init_path = Path::new(base_path).join("databases").join("qdrant").join(".qdrant-initalized").to_str().unwrap().to_string(); + let storage_path = path.join("storage").to_str().unwrap().to_string(); + let snapshot_path = path.join("snapshots").to_str().unwrap().to_string(); + let init_path = path.join(".qdrant-initalized").to_str().unwrap().to_string(); let qdrant_server_environment = HashMap::from_iter([ (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), @@ -88,6 +99,7 @@ pub fn start_qdrant_server() { let server_pid = child.pid(); info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}."); + log_potential_zombie_process(path.join(PID_FILE_NAME), server_pid.to_string().as_str()); // Save the server process to stop it later: *server_spawn_clone.lock().unwrap() = Some(child); @@ -118,7 +130,6 @@ pub fn start_qdrant_server() { /// Stops the Qdrant server process. pub fn stop_qdrant_server() { - drop_tmpdir(); if let Some(server_process) = QDRANT_SERVER.lock().unwrap().take() { let server_kill_result = server_process.kill(); match server_kill_result { @@ -128,10 +139,15 @@ pub fn stop_qdrant_server() { } else { warn!(Source = "Qdrant"; "Qdrant server process was not started or is already stopped."); } + drop_tmpdir(); + if let Err(e) = cleanup_qdrant(){ + warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); + } } -pub fn create_temp_tls_files(path: PathBuf) -> Result<(PathBuf, PathBuf), Box> { - let (certificate, cert_private_key, cert_fingerprint) = generate_certificate(); +/// Create 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); @@ -139,12 +155,12 @@ pub fn create_temp_tls_files(path: PathBuf) -> Result<(PathBuf, PathBuf), Box Result<(), Box> { + let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").join(PID_FILE_NAME); + kill_zombie_process(pid_path, "qdrant.exe")?; + delete_old_certificates()?; + Ok(()) +} + +pub fn delete_old_certificates() -> Result<(), Box> { + let dir_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant"); + + if !dir_path.exists() { + return Ok(()); + } + + for entry in fs::read_dir(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/runtime_certificate.rs b/runtime/src/runtime_certificate.rs index abbde65cb..e4255861b 100644 --- a/runtime/src/runtime_certificate.rs +++ b/runtime/src/runtime_certificate.rs @@ -16,11 +16,11 @@ pub fn generate_runtime_certificate() { info!("Try to generate a TLS certificate for the runtime API server..."); - let (certificate, cer_private_key, cer_fingerprint) = generate_certificate(); + let cert = generate_certificate(); - CERTIFICATE_FINGERPRINT.set(cer_fingerprint).expect("Could not set the certificate fingerprint."); - CERTIFICATE.set(certificate).expect("Could not set the certificate."); - CERTIFICATE_PRIVATE_KEY.set(cer_private_key).expect("Could not set the private key."); + CERTIFICATE_FINGERPRINT.set(cert.fingerprint).expect("Could not set the certificate fingerprint."); + CERTIFICATE.set(cert.certificate).expect("Could not set the certificate."); + CERTIFICATE_PRIVATE_KEY.set(cert.private_key).expect("Could not set the private key."); info!("Done generating certificate for the runtime API server."); } \ No newline at end of file diff --git a/runtime/src/zombie_process_remover.rs b/runtime/src/zombie_process_remover.rs new file mode 100644 index 000000000..33d2ba74e --- /dev/null +++ b/runtime/src/zombie_process_remover.rs @@ -0,0 +1,145 @@ +use std::fs; +use std::fs::File; +use std::io::{Error, ErrorKind, Write}; +use std::path::PathBuf; +use std::process::Command; +use log::{info, warn}; + +#[cfg(unix)] +pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { + // + // Check if PID exists and name matches + // + let ps_output = Command::new("ps") + .arg("-p") + .arg(pid.to_string()) + .arg("-o") + .arg("comm=") + .output()?; + + let output = String::from_utf8_lossy(&ps_output.stdout).trim().to_string(); + + if output.is_empty() { + info!("Pid file {} was found, but process was not.", pid); + return Ok(()); + } + + let name = output; + if name != expected_name { + return Err(Error::new(ErrorKind::InvalidInput, "Process name does not match")); + } + + // + // Kill the process + // + let kill_output = Command::new("kill") + .arg("-9") + .arg(pid.to_string()) + .output()?; + + if !kill_output.status.success() { + return Err(Error::new(ErrorKind::Other, "Failed to kill process")); + } + + // + // Verify process is killed + // + let ps_check = Command::new("ps") + .arg("-p") + .arg(pid.to_string()) + .output()?; + + let output = String::from_utf8_lossy(&ps_check.stdout).trim().to_string(); + if output.is_empty() { + Ok(()) + } else { + Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) + } +} + +#[cfg(windows)] +pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { + // + // Check if PID exists and name matches + // + let tasklist_output = Command::new("tasklist") + .arg("/FI") + .arg(format!("PID eq {}", pid)) + .arg("/FO") + .arg("CSV") + .arg("/NH") + .output()?; + + let output = String::from_utf8_lossy(&tasklist_output.stdout).trim().to_string(); + + if output.is_empty() || !output.starts_with('"') { + info!("Pid file {} was found, but process was not.", pid); + return Ok(()) + } + + let name = output.split(',').next().unwrap_or("").trim_matches('"'); + if name != expected_name { + return Err(Error::new(ErrorKind::InvalidInput, format!("Process name does not match. Expected:{}, got:{}",expected_name,name))); + } + + // + // Kill the process + // + let kill_output = Command::new("taskkill") + .arg("/PID") + .arg(pid.to_string()) + .arg("/F") + .arg("/T") + .output()?; + + if !kill_output.status.success() { + return Err(Error::new(ErrorKind::Other, "Failed to kill process")); + } + + // + // Verify process is killed + // + let tasklist_check = Command::new("tasklist") + .arg("/FI") + .arg(format!("PID eq {}", pid)) + .output()?; + + let output = String::from_utf8_lossy(&tasklist_check.stdout).trim().to_string(); + if output.is_empty() || !output.starts_with('"') { + Ok(()) + } + else { + Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) + } +} + + +pub fn kill_zombie_process(pid_file_path: PathBuf, process_name: &str) -> Result<(), Error> { + if !pid_file_path.exists() { + return Ok(()); + } + + let pid_str = fs::read_to_string(&pid_file_path)?; + let pid: u32 = pid_str.trim().parse().map_err(|_| {Error::new(ErrorKind::InvalidData, "Invalid PID in file")})?; + if let Err(e) = ensure_process_killed(pid, process_name){ + return Err(e); + } + + fs::remove_file(&pid_file_path)?; + info!("Killed qdrant process and deleted redundant Pid file: {}", pid_file_path.display()); + + Ok(()) +} + +pub fn log_potential_zombie_process(pid_file_path: PathBuf, content: &str) { + match File::create(&pid_file_path) { + Ok(mut file) => { + if let Err(e) = file.write_all(content.as_bytes()) { + warn!("Failed to write to {}: {}", pid_file_path.display(), e); + } + } + Err(e) => { + warn!("Failed to create {}: {}", pid_file_path.display(), e); + } + } +} \ No newline at end of file diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index d2cb54f36..8b2cb7033 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -22,7 +22,7 @@ "args": true }, { - "name": "resources/databases/qdrant/qdrant", + "name": "target/databases/qdrant/qdrant", "sidecar": true, "args": true } @@ -65,7 +65,7 @@ "identifier": "com.github.mindwork-ai.ai-studio", "externalBin": [ "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", - "resources/databases/qdrant/qdrant" + "target/databases/qdrant/qdrant" ], "resources": [ "resources/*" From 3589ec0cc2a9e7640b25c6190bab2d96420aa032 Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Fri, 16 Jan 2026 21:47:33 +0100 Subject: [PATCH 15/30] added a minor cleanup --- app/MindWork AI Studio/Assistants/I18N/allTexts.lua | 9 +++++++++ runtime/build.rs | 7 ------- runtime/src/app_window.rs | 3 +-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 1ec8c0222..2a0a41f0e 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -4930,6 +4930,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1282228996"] = "AI Studio runs w -- 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." @@ -4969,6 +4972,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is -- 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::INFORMATION::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::INFORMATION::T2029659664"] = "Copies the following to the clipboard" + -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" @@ -5146,6 +5152,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this libra -- 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::INFORMATION::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::INFORMATION::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::INFORMATION::T986578435"] = "Install Pandoc" diff --git a/runtime/build.rs b/runtime/build.rs index afa013cf1..e90dbed88 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -25,13 +25,6 @@ fn main() { }, Err(_) => { println!("cargo: warning= The environment variable 'MINDWORK_START_DEV_ENV' was not found."); - if let Err(e) = kill_zombie_qdrant_process(){ - println!("cargo:warning=Error: {e}"); - return; - }; - if let Err(e) = delete_old_certificates() { - println!("cargo: warning= Failed to delete old certificates: {e}"); - } } } tauri_build::build(); diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index dc254cd4a..3c5647440 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -1,4 +1,3 @@ -use std::path::Path; use std::sync::Mutex; use std::time::Duration; use log::{debug, error, info, trace, warn}; @@ -14,7 +13,7 @@ use tauri::api::dialog::blocking::FileDialogBuilder; use tokio::sync::broadcast; use tokio::time; use crate::api_token::APIToken; -use crate::dotnet::{cleanup_dotnet_server, stop_dotnet_server, PID_FILE_NAME}; +use crate::dotnet::{cleanup_dotnet_server, 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; From ccb4967725b9ff5073d06fabc3696dc4d2fa4495 Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Tue, 27 Jan 2026 15:42:38 +0100 Subject: [PATCH 16/30] fixed github build pipeline --- .github/workflows/build-and-release.yml | 32 +++++++++++++------------ metadata.txt | 3 ++- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index cf8daf457..329b58f6c 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -2,7 +2,7 @@ name: Build and Release on: push: branches: - - main + - "**" tags: - "v*.*.*" @@ -247,7 +247,7 @@ jobs: $pdfium_version = $pdfium_version.Split('.')[2] # Next line is the necessary Qdrant version: - $qdrant_version = "v$metadata[12]" + $qdrant_version = "v$($metadata[11])" # Write the metadata to the environment: Write-Output "APP_VERSION=${app_version}" >> $env:GITHUB_ENV @@ -407,33 +407,34 @@ jobs: env: QDRANT_VERSION: ${{ env.QDRANT_VERSION }} DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }} + RUST_TARGET: ${{ matrix.rust_target }} run: | set -e # Target directory: - TDB_DIR="runtime/resources/databases/qdrant" + 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" + DB_TARGET="qdrant-${RUST_TARGET}" ;; linux-arm64) QDRANT_FILE="aarch64-unknown-linux-musl.tar.gz" DB_SOURCE="qdrant" - DB_TARGET="qdrant" + DB_TARGET="qdrant-${RUST_TARGET}" ;; osx-x64) QDRANT_FILE="x86_64-apple-darwin.tar.gz" DB_SOURCE="qdrant" - DB_TARGET="qdrant" + DB_TARGET="qdrant-${RUST_TARGET}" ;; osx-arm64) QDRANT_FILE="aarch64-apple-darwin.tar.gz" DB_SOURCE="qdrant" - DB_TARGET="qdrant" + DB_TARGET="qdrant-${RUST_TARGET}" ;; *) echo "Unknown platform: ${DOTNET_RUNTIME}" @@ -441,7 +442,7 @@ jobs: ;; esac - QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/v${QDRANT_VERSION}/qdrant-{QDRANT_FILE}" + QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSION}/qdrant-${QDRANT_FILE}" echo "Download Qdrant $QDRANT_URL ..." TMP=$(mktemp -d) @@ -469,20 +470,21 @@ jobs: env: QDRANT_VERSION: ${{ env.QDRANT_VERSION }} DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }} + RUST_TARGET: ${{ matrix.rust_target }} run: | - $TDB_DIR = "runtime\resources\databases\qdrant" + $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.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.exe"" + $DB_TARGET = "qdrant-$($env:RUST_TARGET).exe" } default { Write-Error "Unknown platform: $($env:DOTNET_RUNTIME)" @@ -490,7 +492,7 @@ jobs: } } - QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/v${QDRANT_VERSION}/qdrant-{QDRANT_FILE}" + $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) @@ -629,7 +631,7 @@ jobs: cd runtime export TAURI_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY" export TAURI_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD" - cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }} + cargo tauri build --target ${{ matrix.rust_target }} --bundles none - name: Build Tauri project (Windows) if: matrix.platform == 'windows-latest' @@ -640,7 +642,7 @@ jobs: cd runtime $env:TAURI_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY" $env:TAURI_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD" - cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }} + cargo tauri build --target ${{ matrix.rust_target }} --bundles none - name: Upload artifact (macOS) if: startsWith(matrix.platform, 'macos') && startsWith(github.ref, 'refs/tags/v') @@ -951,4 +953,4 @@ jobs: name: "Release ${{ env.FORMATTED_VERSION }}" fail_on_unmatched_files: true files: | - release/assets/* \ No newline at end of file + release/assets/* diff --git a/metadata.txt b/metadata.txt index 3f8f7b4fc..6bc2c67b4 100644 --- a/metadata.txt +++ b/metadata.txt @@ -7,5 +7,6 @@ 8.15.0 1.8.1 37293e4a7cb, release -osx-arm64 +win-x64 144.0.7543.0 +1.16.3 \ No newline at end of file From 91bf83ea1229e19f58f572ca701df72d46cf9f23 Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Wed, 28 Jan 2026 13:28:32 +0100 Subject: [PATCH 17/30] fixed jemalloc error for macOS --- runtime/src/qdrant.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 5be5720a2..523a3c2a9 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -76,7 +76,7 @@ pub fn start_qdrant_server(){ let snapshot_path = path.join("snapshots").to_str().unwrap().to_string(); let init_path = path.join(".qdrant-initalized").to_str().unwrap().to_string(); - let qdrant_server_environment = HashMap::from_iter([ + let mut qdrant_server_environment = HashMap::from_iter([ (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), (String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()), (String::from("QDRANT_INIT_FILE_PATH"), init_path), @@ -90,6 +90,13 @@ pub fn start_qdrant_server(){ let server_spawn_clone = QDRANT_SERVER.clone(); tauri::async_runtime::spawn(async move { + #[cfg(target_os = "macos")] + { + qdrant_server_environment.insert( + "MALLOC_CONF".to_string(), + "background_thread:false".to_string(), + ); + } let (mut rx, child) = Command::new_sidecar("qdrant") .expect("Failed to create sidecar for Qdrant") .args(["--config-path", "resources/databases/qdrant/config.yaml"]) From 223d288ab40e53ee4cfa085149bbe801bbd86c31 Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Fri, 30 Jan 2026 17:59:15 +0100 Subject: [PATCH 18/30] fixed issues with stale processes --- .github/workflows/build-and-release.yml | 6 +- .../Pages/Information.razor | 2 + runtime/Cargo.toml | 3 +- runtime/build.rs | 191 +----------------- runtime/src/app_window.rs | 30 +-- runtime/src/dotnet.rs | 7 +- runtime/src/lib.rs | 2 +- runtime/src/main.rs | 10 - runtime/src/qdrant.rs | 20 +- runtime/src/stale_process_cleanup.rs | 94 +++++++++ runtime/src/zombie_process_remover.rs | 145 ------------- 11 files changed, 136 insertions(+), 374 deletions(-) create mode 100644 runtime/src/stale_process_cleanup.rs delete mode 100644 runtime/src/zombie_process_remover.rs diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 329b58f6c..091faafba 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -2,7 +2,7 @@ name: Build and Release on: push: branches: - - "**" + - main tags: - "v*.*.*" @@ -631,7 +631,7 @@ jobs: cd runtime export TAURI_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY" export TAURI_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD" - cargo tauri build --target ${{ matrix.rust_target }} --bundles none + cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }} - name: Build Tauri project (Windows) if: matrix.platform == 'windows-latest' @@ -642,7 +642,7 @@ jobs: cd runtime $env:TAURI_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY" $env:TAURI_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD" - cargo tauri build --target ${{ matrix.rust_target }} --bundles none + cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }} - name: Upload artifact (macOS) if: startsWith(matrix.platform, 'macos') && startsWith(github.ref, 'refs/tags/v') diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 54087f163..8d33571f1 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -238,6 +238,8 @@ + + diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 9fc375cdc..b9a7dfdae 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -7,7 +7,6 @@ authors = ["Thorsten Sommer"] [build-dependencies] tauri-build = { version = "1.5", features = [] } -dirs = "6.0.0" [dependencies] tauri = { version = "1.8", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] } @@ -42,6 +41,7 @@ cfg-if = "1.0.4" pptx-to-md = "0.4.0" tempfile = "3.8" strum_macros = "0.27" +sysinfo = "0.38.0" # Fixes security vulnerability downstream, where the upstream is not fixed yet: url = "2.5.8" @@ -50,6 +50,7 @@ crossbeam-channel = "0.5.15" tracing-subscriber = "0.3.20" dirs = "6.0.0" + [target.'cfg(target_os = "linux")'.dependencies] # See issue https://github.com/tauri-apps/tauri/issues/4470 reqwest = { version = "0.13.1", features = ["native-tls-vendored"] } diff --git a/runtime/build.rs b/runtime/build.rs index e90dbed88..c4d1f7498 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -1,32 +1,6 @@ -use std::{env, fs}; use std::path::{PathBuf}; -use std::process::Command; -use std::io::{Error, ErrorKind}; fn main() { - match env::var("MINDWORK_START_DEV_ENV") { - Ok(val) => { - let is_started_manually = match val.parse::() { - Ok(b) => b, - Err(_) => { - println!("cargo: warning= Invalid value for MINDWORK_START_DEV_ENV: expected 'true' or 'false'"); - return; - } - }; - if is_started_manually { - if let Err(e) = kill_zombie_qdrant_process(){ - println!("cargo:warning=Error: {e}"); - return; - }; - if let Err(e) = delete_old_certificates() { - println!("cargo: warning= Failed to delete old certificates: {e}"); - } - } - }, - Err(_) => { - println!("cargo: warning= The environment variable 'MINDWORK_START_DEV_ENV' was not found."); - } - } tauri_build::build(); let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); @@ -109,167 +83,4 @@ fn update_tauri_conf(tauri_conf_path: &str, version: &str) { } std::fs::write(tauri_conf_path, new_tauri_conf).unwrap(); -} - -#[cfg(unix)] -pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { - // - // Check if PID exists and name matches - // - let ps_output = Command::new("ps") - .arg("-p") - .arg(pid.to_string()) - .arg("-o") - .arg("comm=") - .output()?; - - let output = String::from_utf8_lossy(&ps_output.stdout).trim().to_string(); - - if output.is_empty() { - // Process doesn't exist - return Ok(()); - } - - let name = output; - if name != expected_name { - return Err(Error::new(ErrorKind::InvalidInput, "Process name does not match")); - } - - // - // Kill the process - // - let kill_output = Command::new("kill") - .arg("-9") - .arg(pid.to_string()) - .output()?; - - if !kill_output.status.success() { - return Err(Error::new(ErrorKind::Other, "Failed to kill process")); - } - - // - // Verify process is killed - // - let ps_check = Command::new("ps") - .arg("-p") - .arg(pid.to_string()) - .output()?; - - let output = String::from_utf8_lossy(&ps_check.stdout).trim().to_string(); - if output.is_empty() { - Ok(()) - } else { - Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) - } -} - -#[cfg(windows)] -pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { - // - // Check if PID exists and name matches - // - let tasklist_output = Command::new("tasklist") - .arg("/FI") - .arg(format!("PID eq {}", pid)) - .arg("/FO") - .arg("CSV") - .arg("/NH") - .output()?; - - let output = String::from_utf8_lossy(&tasklist_output.stdout).trim().to_string(); - - if output.is_empty() || !output.starts_with('"') { - println!("cargo:warning= Pid file was found, but process was not."); - return Ok(()) - } - - let name = output.split(',').next().unwrap_or("").trim_matches('"'); - if name != expected_name { - return Err(Error::new(ErrorKind::InvalidInput, format!("Process name does not match. Expected:{}, got:{}",expected_name,name))); - } - - // - // Kill the process - // - let kill_output = Command::new("taskkill") - .arg("/PID") - .arg(pid.to_string()) - .arg("/F") - .arg("/T") - .output()?; - - if !kill_output.status.success() { - return Err(Error::new(ErrorKind::Other, "Failed to kill process")); - } - - // - // Verify process is killed - // - let tasklist_check = Command::new("tasklist") - .arg("/FI") - .arg(format!("PID eq {}", pid)) - .output()?; - - let output = String::from_utf8_lossy(&tasklist_check.stdout).trim().to_string(); - if output.is_empty() || !output.starts_with('"') { - Ok(()) - } - else { - Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) - } -} - - -pub fn kill_zombie_qdrant_process() -> Result<(), Error> { - let pid_file = dirs::data_local_dir() - .expect("Local appdata was not found") - .join("com.github.mindwork-ai.ai-studio") - .join("data") - .join("databases") - .join("qdrant") - .join("qdrant.pid"); - - if !pid_file.exists() { - return Ok(()); - } - - let pid_str = fs::read_to_string(&pid_file)?; - let pid: u32 = pid_str.trim().parse().map_err(|_| {Error::new(ErrorKind::InvalidData, "Invalid PID in file")})?; - if let Err(e) = ensure_process_killed(pid, "qdrant.exe".as_ref()){ - return Err(e); - } - - fs::remove_file(&pid_file)?; - println!("cargo:warning= Killed qdrant process and deleted redundant Pid file: {}", pid_file.display()); - - Ok(()) -} - -pub fn delete_old_certificates() -> Result<(), Box> { - let dir_path = dirs::data_local_dir() - .expect("Local appdata was not found") - .join("com.github.mindwork-ai.ai-studio") - .join("data") - .join("databases") - .join("qdrant"); - - if !dir_path.exists() { - return Ok(()); - } - - for entry in fs::read_dir(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)?; - println!("cargo: warning= Removed old certificates in: {}", path.display()); - } - } - } - Ok(()) -} +} \ No newline at end of file diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index df48fc2b9..c7e7bd740 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -15,11 +15,13 @@ use tauri::api::dialog::blocking::FileDialogBuilder; use tokio::sync::broadcast; use tokio::time; use crate::api_token::APIToken; -use crate::dotnet::{cleanup_dotnet_server, stop_dotnet_server}; +use crate::dotnet::{cleanup_dotnet_server, start_dotnet_server, 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::{cleanup_qdrant, start_qdrant_server, stop_qdrant_server}; +#[cfg(debug_assertions)] +use crate::dotnet::create_startup_env_file; /// The Tauri main window. static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)); @@ -102,20 +104,24 @@ pub fn start_tauri() { let data_path = data_path.join("data"); // Get and store the data and config directories: - DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not abe to set the data directory.")).unwrap(); + DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the data directory.")).unwrap(); CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap(); - if is_prod() { - cleanup_qdrant().expect("Zombie processes of Qdrant were not killed"); - cleanup_dotnet_server(); + cleanup_qdrant(); + cleanup_dotnet_server(); + + if is_dev() { + #[cfg(debug_assertions)] + create_startup_env_file(); + } else { + start_dotnet_server(); } + start_qdrant_server(); 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()) @@ -164,6 +170,7 @@ pub fn start_tauri() { if is_prod() { stop_dotnet_server(); + stop_qdrant_server(); } else { warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server."); } @@ -193,6 +200,10 @@ pub fn start_tauri() { RunEvent::ExitRequested { .. } => { warn!(Source = "Tauri"; "Run event: exit was requested."); stop_qdrant_server(); + if is_prod() { + warn!("Try to stop the .NET server as well..."); + stop_dotnet_server(); + } } RunEvent::Ready => { @@ -204,10 +215,6 @@ pub fn start_tauri() { }); warn!(Source = "Tauri"; "Tauri app was stopped."); - if is_prod() { - warn!("Try to stop the .NET server as well..."); - stop_dotnet_server(); - } } /// Our event API endpoint for Tauri events. We try to send an endless stream of events to the client. @@ -458,7 +465,6 @@ pub async fn install_update(_token: APIToken) { let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone(); match cloned_response_option { Some(update_response) => { - stop_qdrant_server(); update_response.download_and_install().await.unwrap(); }, diff --git a/runtime/src/dotnet.rs b/runtime/src/dotnet.rs index 944e1d952..c1e0b5607 100644 --- a/runtime/src/dotnet.rs +++ b/runtime/src/dotnet.rs @@ -16,7 +16,7 @@ use crate::encryption::ENCRYPTION; use crate::environment::{is_dev, DATA_DIRECTORY}; use crate::network::get_available_port; use crate::runtime_api::API_SERVER_PORT; -use crate::zombie_process_remover::{kill_zombie_process, log_potential_zombie_process}; +use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; // The .NET server is started in a separate process and communicates with this // runtime process via IPC. However, we do net start the .NET server in @@ -100,7 +100,7 @@ pub fn start_dotnet_server() { .expect("Failed to spawn .NET server process."); let server_pid = child.pid(); info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_pid}."); - log_potential_zombie_process(Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME), server_pid.to_string().as_str()); + log_potential_stale_process(Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME), server_pid); // Save the server process to stop it later: *server_spawn_clone.lock().unwrap() = Some(child); @@ -158,13 +158,14 @@ pub fn stop_dotnet_server() { } else { warn!("The .NET server process was not started or is already stopped."); } + info!("Start dotnet server cleanup"); cleanup_dotnet_server(); } /// Remove old Pid files and kill the corresponding processes pub fn cleanup_dotnet_server() { let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME); - if let Err(e) = kill_zombie_process(pid_path, "mindworkAIStudioServer.exe"){ + if let Err(e) = kill_stale_process(pid_path) { warn!(Source = ".NET"; "Error during the cleanup of .NET: {}", e); } } \ No newline at end of file diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 65b6abdf8..5447395be 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -16,4 +16,4 @@ pub mod pandoc; pub mod qdrant; pub mod certificate_factory; pub mod runtime_api_token; -pub mod zombie_process_remover; \ No newline at end of file +pub mod stale_process_cleanup; \ No newline at end of file diff --git a/runtime/src/main.rs b/runtime/src/main.rs index 91427472c..00a7ba905 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -7,14 +7,11 @@ extern crate core; use log::{info, warn}; use mindwork_ai_studio::app_window::start_tauri; use mindwork_ai_studio::runtime_certificate::{generate_runtime_certificate}; -use mindwork_ai_studio::dotnet::start_dotnet_server; use mindwork_ai_studio::environment::is_dev; use mindwork_ai_studio::log::init_logging; use mindwork_ai_studio::metadata::MetaData; use mindwork_ai_studio::runtime_api::start_runtime_api; -#[cfg(debug_assertions)] -use mindwork_ai_studio::dotnet::create_startup_env_file; #[tokio::main] async fn main() { @@ -49,12 +46,5 @@ async fn main() { generate_runtime_certificate(); start_runtime_api(); - if is_dev() { - #[cfg(debug_assertions)] - create_startup_env_file(); - } else { - start_dotnet_server(); - } - start_tauri(); } \ No newline at end of file diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 523a3c2a9..c562dc753 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -16,7 +16,7 @@ use crate::environment::DATA_DIRECTORY; use crate::certificate_factory::generate_certificate; use std::path::PathBuf; use tempfile::{TempDir, Builder}; -use crate::zombie_process_remover::{kill_zombie_process, log_potential_zombie_process}; +use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; // Qdrant server process started in a separate process and can communicate // via HTTP or gRPC with the .NET server and the runtime process @@ -106,7 +106,7 @@ pub fn start_qdrant_server(){ let server_pid = child.pid(); info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}."); - log_potential_zombie_process(path.join(PID_FILE_NAME), server_pid.to_string().as_str()); + log_potential_stale_process(path.join(PID_FILE_NAME), server_pid); // Save the server process to stop it later: *server_spawn_clone.lock().unwrap() = Some(child); @@ -150,9 +150,7 @@ pub fn stop_qdrant_server() { } drop_tmpdir(); - if let Err(e) = cleanup_qdrant(){ - warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); - } + cleanup_qdrant(); } /// Create temporary directory with TLS relevant files @@ -193,11 +191,15 @@ pub fn drop_tmpdir() { } /// Remove old Pid files and kill the corresponding processes -pub fn cleanup_qdrant() -> Result<(), Box> { +pub fn cleanup_qdrant() { let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").join(PID_FILE_NAME); - kill_zombie_process(pid_path, "qdrant.exe")?; - delete_old_certificates()?; - Ok(()) + if let Err(e) = kill_stale_process(pid_path) { + warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); + } + if let Err(e) = delete_old_certificates() { + warn!(Source = "Qdrant"; "Error during the cleanup of Qdrant: {}", e); + } + } pub fn delete_old_certificates() -> Result<(), Box> { diff --git a/runtime/src/stale_process_cleanup.rs b/runtime/src/stale_process_cleanup.rs new file mode 100644 index 000000000..124f576c1 --- /dev/null +++ b/runtime/src/stale_process_cleanup.rs @@ -0,0 +1,94 @@ +use std::fs; +use std::fs::File; +use std::io::{Error, ErrorKind, Write}; +use std::path::{PathBuf}; +use log::{info, warn}; +use sysinfo::{Pid, ProcessesToUpdate, Signal, System}; + +fn parse_pid_file(content: &str) -> Result<(u32, String), Error> { + let mut lines = content + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()); + let pid_str = lines + .next() + .ok_or_else(|| Error::new(ErrorKind::InvalidData, "Missing PID in file"))?; + let pid: u32 = pid_str + .parse() + .map_err(|_| Error::new(ErrorKind::InvalidData, "Invalid PID in file"))?; + let name = lines + .next() + .ok_or_else(|| Error::new(ErrorKind::InvalidData, "Missing process name in file"))? + .to_string(); + Ok((pid, name)) +} + +pub fn kill_stale_process(pid_file_path: PathBuf) -> Result<(), Error> { + if !pid_file_path.exists() { + return Ok(()); + } + + let pid_file_content = fs::read_to_string(&pid_file_path)?; + let (pid, expected_name) = parse_pid_file(&pid_file_content)?; + + let mut system = System::new_all(); + + let pid = Pid::from_u32(pid); + system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); + if let Some(process) = system.process(pid){ + let name = process.name().to_string_lossy(); + if name != expected_name { + return Err(Error::new( + ErrorKind::InvalidInput, + format!( + "Process name does not match: expected '{}' but found '{}'", + expected_name, name + ), + )); + } + + let killed = process.kill_with(Signal::Kill).unwrap_or_else(|| process.kill()); + if !killed { + return Err(Error::new(ErrorKind::Other, "Failed to kill process")); + } + + system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); + if !system.process(pid).is_none() { + return Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) + } + info!("Killed process: {}", pid_file_path.display()); + } else { + info!("Pid file {} was found, but process was not.", pid); + }; + + fs::remove_file(&pid_file_path)?; + info!("Deleted redundant Pid file: {}", pid_file_path.display()); + Ok(()) +} + +pub fn log_potential_stale_process(pid_file_path: PathBuf, pid: u32) { + let mut system = System::new_all(); + let pid_u32 = pid; + let pid = Pid::from_u32(pid_u32); + system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); + let Some(process) = system.process(pid) else { + warn!( + "Pid file {} was not created because the process was not found.", + pid_u32 + ); + return; + }; + + match File::create(&pid_file_path) { + Ok(mut file) => { + let name = process.name().to_string_lossy(); + let content = format!("{pid_u32}\n{name}\n"); + if let Err(e) = file.write_all(content.as_bytes()) { + warn!("Failed to write to {}: {}", pid_file_path.display(), e); + } + } + Err(e) => { + warn!("Failed to create {}: {}", pid_file_path.display(), e); + } + } +} diff --git a/runtime/src/zombie_process_remover.rs b/runtime/src/zombie_process_remover.rs deleted file mode 100644 index 33d2ba74e..000000000 --- a/runtime/src/zombie_process_remover.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::fs; -use std::fs::File; -use std::io::{Error, ErrorKind, Write}; -use std::path::PathBuf; -use std::process::Command; -use log::{info, warn}; - -#[cfg(unix)] -pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { - // - // Check if PID exists and name matches - // - let ps_output = Command::new("ps") - .arg("-p") - .arg(pid.to_string()) - .arg("-o") - .arg("comm=") - .output()?; - - let output = String::from_utf8_lossy(&ps_output.stdout).trim().to_string(); - - if output.is_empty() { - info!("Pid file {} was found, but process was not.", pid); - return Ok(()); - } - - let name = output; - if name != expected_name { - return Err(Error::new(ErrorKind::InvalidInput, "Process name does not match")); - } - - // - // Kill the process - // - let kill_output = Command::new("kill") - .arg("-9") - .arg(pid.to_string()) - .output()?; - - if !kill_output.status.success() { - return Err(Error::new(ErrorKind::Other, "Failed to kill process")); - } - - // - // Verify process is killed - // - let ps_check = Command::new("ps") - .arg("-p") - .arg(pid.to_string()) - .output()?; - - let output = String::from_utf8_lossy(&ps_check.stdout).trim().to_string(); - if output.is_empty() { - Ok(()) - } else { - Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) - } -} - -#[cfg(windows)] -pub fn ensure_process_killed(pid: u32, expected_name: &str) -> Result<(), Error> { - // - // Check if PID exists and name matches - // - let tasklist_output = Command::new("tasklist") - .arg("/FI") - .arg(format!("PID eq {}", pid)) - .arg("/FO") - .arg("CSV") - .arg("/NH") - .output()?; - - let output = String::from_utf8_lossy(&tasklist_output.stdout).trim().to_string(); - - if output.is_empty() || !output.starts_with('"') { - info!("Pid file {} was found, but process was not.", pid); - return Ok(()) - } - - let name = output.split(',').next().unwrap_or("").trim_matches('"'); - if name != expected_name { - return Err(Error::new(ErrorKind::InvalidInput, format!("Process name does not match. Expected:{}, got:{}",expected_name,name))); - } - - // - // Kill the process - // - let kill_output = Command::new("taskkill") - .arg("/PID") - .arg(pid.to_string()) - .arg("/F") - .arg("/T") - .output()?; - - if !kill_output.status.success() { - return Err(Error::new(ErrorKind::Other, "Failed to kill process")); - } - - // - // Verify process is killed - // - let tasklist_check = Command::new("tasklist") - .arg("/FI") - .arg(format!("PID eq {}", pid)) - .output()?; - - let output = String::from_utf8_lossy(&tasklist_check.stdout).trim().to_string(); - if output.is_empty() || !output.starts_with('"') { - Ok(()) - } - else { - Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) - } -} - - -pub fn kill_zombie_process(pid_file_path: PathBuf, process_name: &str) -> Result<(), Error> { - if !pid_file_path.exists() { - return Ok(()); - } - - let pid_str = fs::read_to_string(&pid_file_path)?; - let pid: u32 = pid_str.trim().parse().map_err(|_| {Error::new(ErrorKind::InvalidData, "Invalid PID in file")})?; - if let Err(e) = ensure_process_killed(pid, process_name){ - return Err(e); - } - - fs::remove_file(&pid_file_path)?; - info!("Killed qdrant process and deleted redundant Pid file: {}", pid_file_path.display()); - - Ok(()) -} - -pub fn log_potential_zombie_process(pid_file_path: PathBuf, content: &str) { - match File::create(&pid_file_path) { - Ok(mut file) => { - if let Err(e) = file.write_all(content.as_bytes()) { - warn!("Failed to write to {}: {}", pid_file_path.display(), e); - } - } - Err(e) => { - warn!("Failed to create {}: {}", pid_file_path.display(), e); - } - } -} \ No newline at end of file From b949660b0960a58a880a766cea0847c6f793059c Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 2 Feb 2026 13:06:11 +0100 Subject: [PATCH 19/30] Removed no longer needed imports --- app/Build/Commands/Qdrant.cs | 3 +-- app/MindWork AI Studio/Program.cs | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs index 3693c6f8a..29369ccf3 100644 --- a/app/Build/Commands/Qdrant.cs +++ b/app/Build/Commands/Qdrant.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.Eventing.Reader; -using System.Formats.Tar; +using System.Formats.Tar; using System.IO.Compression; using SharedTools; diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 9f91cb55f..c522d8011 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -3,7 +3,6 @@ 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; From 62758ae1a280bbc0ad7d391fec3642fe8b196e3c Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 2 Feb 2026 13:10:44 +0100 Subject: [PATCH 20/30] Removed redundant default --- app/MindWork AI Studio/Pages/Information.razor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 423492010..ef2a64a27 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -67,7 +67,7 @@ public partial class Information : MSGComponentBase private bool showEnterpriseConfigDetails; - private bool showDatabaseDetails = false; + private bool showDatabaseDetails; private IPluginMetadata? configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION); From 95c6d19c086a5cd89330f004d0324040785f8c20 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 2 Feb 2026 13:11:22 +0100 Subject: [PATCH 21/30] Refactored list to use a data class instead of a tuple --- app/MindWork AI Studio/Pages/Information.razor | 6 +++--- app/MindWork AI Studio/Pages/Information.razor.cs | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 8d33571f1..bc7645609 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -25,12 +25,12 @@ - @foreach (var (label, value) in DatabaseDisplayInfo) + @foreach (var item in this.databaseDisplayInfo) {
- @label: @value - + @item.Label: @item.Value +
}
diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index ef2a64a27..aa649a3c2 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -71,7 +71,9 @@ public partial class Information : MSGComponentBase private IPluginMetadata? configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION); - private List<(string Label, string Value)> DatabaseDisplayInfo = new(); + private sealed record DatabaseDisplayInfo(string Label, string Value); + + private readonly List databaseDisplayInfo = new(); /// /// Determines whether the enterprise configuration has details that can be shown/hidden. @@ -107,9 +109,9 @@ protected override async Task OnInitializedAsync() this.osLanguage = await this.RustService.ReadUserLanguage(); this.logPaths = await this.RustService.GetLogPaths(); - await foreach (var item in this.DatabaseClient.GetDisplayInfo()) + await foreach (var (label, value) in this.DatabaseClient.GetDisplayInfo()) { - this.DatabaseDisplayInfo.Add(item); + this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value)); } // Determine the Pandoc version may take some time, so we start it here From 0dd7d65efc8a76dc5a92151aee68e237d2075e40 Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Mon, 2 Feb 2026 18:02:32 +0100 Subject: [PATCH 22/30] Improved logging behaviour --- .../Pages/Information.razor | 2 +- documentation/Build.md | 14 ++++++++- runtime/src/dotnet.rs | 6 ++-- runtime/src/lib.rs | 3 +- runtime/src/qdrant.rs | 15 +++------ runtime/src/sidecar_types.rs | 15 +++++++++ runtime/src/stale_process_cleanup.rs | 31 ++++++++----------- 7 files changed, 53 insertions(+), 33 deletions(-) create mode 100644 runtime/src/sidecar_types.rs diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index bc7645609..cd97d4573 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -239,7 +239,7 @@ - + diff --git a/documentation/Build.md b/documentation/Build.md index 21063eef4..582270e05 100644 --- a/documentation/Build.md +++ b/documentation/Build.md @@ -45,7 +45,19 @@ Do you want to test your changes before creating a PR? Follow these steps: 9. Execute the command `dotnet run`. 10. After compiling the .NET code, the app will finally start inside the Tauri runtime window. -You can now test your changes. +You can now test your changes. To stop the application: +- Close the Tauri window (GUI). +- 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 restarting Tauri: +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: diff --git a/runtime/src/dotnet.rs b/runtime/src/dotnet.rs index c1e0b5607..338074a0e 100644 --- a/runtime/src/dotnet.rs +++ b/runtime/src/dotnet.rs @@ -17,6 +17,7 @@ use crate::environment::{is_dev, DATA_DIRECTORY}; use crate::network::get_available_port; use crate::runtime_api::API_SERVER_PORT; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; +use crate::sidecar_types::SidecarType; // The .NET server is started in a separate process and communicates with this // runtime process via IPC. However, we do net start the .NET server in @@ -30,6 +31,7 @@ static DOTNET_SERVER_PORT: Lazy = Lazy::new(|| get_available_port().unwrap( static DOTNET_INITIALIZED: Lazy> = Lazy::new(|| Mutex::new(false)); pub const PID_FILE_NAME: &str = "mindwork_ai_studio.pid"; +const SIDECAR_TYPE:SidecarType = SidecarType::Dotnet; /// Returns the desired port of the .NET server. Our .NET app calls this endpoint to get /// the port where the .NET server should listen to. @@ -100,7 +102,7 @@ pub fn start_dotnet_server() { .expect("Failed to spawn .NET server process."); let server_pid = child.pid(); info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_pid}."); - log_potential_stale_process(Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME), server_pid); + log_potential_stale_process(Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME), server_pid, SIDECAR_TYPE); // Save the server process to stop it later: *server_spawn_clone.lock().unwrap() = Some(child); @@ -165,7 +167,7 @@ pub fn stop_dotnet_server() { /// Remove old Pid files and kill the corresponding processes pub fn cleanup_dotnet_server() { let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME); - if let Err(e) = kill_stale_process(pid_path) { + if let Err(e) = kill_stale_process(pid_path, SIDECAR_TYPE) { warn!(Source = ".NET"; "Error during the cleanup of .NET: {}", e); } } \ No newline at end of file diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 5447395be..1b13e0991 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -16,4 +16,5 @@ pub mod pandoc; pub mod qdrant; pub mod certificate_factory; pub mod runtime_api_token; -pub mod stale_process_cleanup; \ No newline at end of file +pub mod stale_process_cleanup; +mod sidecar_types; \ No newline at end of file diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index c562dc753..41429431f 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -17,6 +17,7 @@ use crate::certificate_factory::generate_certificate; use std::path::PathBuf; use tempfile::{TempDir, Builder}; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; +use crate::sidecar_types::SidecarType; // Qdrant server process started in a separate process and can communicate // via HTTP or gRPC with the .NET server and the runtime process @@ -39,6 +40,7 @@ static API_TOKEN: Lazy = Lazy::new(|| { static TMPDIR: Lazy>> = Lazy::new(|| Mutex::new(None)); const PID_FILE_NAME: &str = "qdrant.pid"; +const SIDECAR_TYPE:SidecarType = SidecarType::Qdrant; #[derive(Serialize)] pub struct ProvideQdrantInfo { @@ -76,7 +78,7 @@ pub fn start_qdrant_server(){ let snapshot_path = path.join("snapshots").to_str().unwrap().to_string(); let init_path = path.join(".qdrant-initalized").to_str().unwrap().to_string(); - let mut qdrant_server_environment = HashMap::from_iter([ + let qdrant_server_environment = HashMap::from_iter([ (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), (String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()), (String::from("QDRANT_INIT_FILE_PATH"), init_path), @@ -90,13 +92,6 @@ pub fn start_qdrant_server(){ let server_spawn_clone = QDRANT_SERVER.clone(); tauri::async_runtime::spawn(async move { - #[cfg(target_os = "macos")] - { - qdrant_server_environment.insert( - "MALLOC_CONF".to_string(), - "background_thread:false".to_string(), - ); - } let (mut rx, child) = Command::new_sidecar("qdrant") .expect("Failed to create sidecar for Qdrant") .args(["--config-path", "resources/databases/qdrant/config.yaml"]) @@ -106,7 +101,7 @@ pub fn start_qdrant_server(){ 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); + 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); @@ -193,7 +188,7 @@ pub fn drop_tmpdir() { /// Remove old Pid files and kill the corresponding processes pub fn cleanup_qdrant() { let pid_path = Path::new(DATA_DIRECTORY.get().unwrap()).join("databases").join("qdrant").join(PID_FILE_NAME); - if let Err(e) = kill_stale_process(pid_path) { + 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() { diff --git a/runtime/src/sidecar_types.rs b/runtime/src/sidecar_types.rs new file mode 100644 index 000000000..7e5bfde0b --- /dev/null +++ b/runtime/src/sidecar_types.rs @@ -0,0 +1,15 @@ +use std::fmt; + +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/src/stale_process_cleanup.rs b/runtime/src/stale_process_cleanup.rs index 124f576c1..7d177ac85 100644 --- a/runtime/src/stale_process_cleanup.rs +++ b/runtime/src/stale_process_cleanup.rs @@ -4,6 +4,7 @@ use std::io::{Error, ErrorKind, Write}; use std::path::{PathBuf}; use log::{info, warn}; use sysinfo::{Pid, ProcessesToUpdate, Signal, System}; +use crate::sidecar_types::SidecarType; fn parse_pid_file(content: &str) -> Result<(u32, String), Error> { let mut lines = content @@ -23,7 +24,7 @@ fn parse_pid_file(content: &str) -> Result<(u32, String), Error> { Ok((pid, name)) } -pub fn kill_stale_process(pid_file_path: PathBuf) -> Result<(), Error> { +pub fn kill_stale_process(pid_file_path: PathBuf, sidecar_type: SidecarType) -> Result<(), Error> { if !pid_file_path.exists() { return Ok(()); } @@ -51,30 +52,24 @@ pub fn kill_stale_process(pid_file_path: PathBuf) -> Result<(), Error> { if !killed { return Err(Error::new(ErrorKind::Other, "Failed to kill process")); } - - system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); - if !system.process(pid).is_none() { - return Err(Error::new(ErrorKind::Other, "Process still running after kill attempt")) - } - info!("Killed process: {}", pid_file_path.display()); + info!(Source="Stale Process Cleanup";"{}: Killed process: \"{}\"", sidecar_type,pid_file_path.display()); } else { - info!("Pid file {} was found, but process was not.", pid); + info!(Source="Stale Process Cleanup";"{}: Pid file with process number '{}' was found, but process was not.", sidecar_type, pid); }; fs::remove_file(&pid_file_path)?; - info!("Deleted redundant Pid file: {}", pid_file_path.display()); + info!(Source="Stale Process Cleanup";"{}: Deleted redundant Pid file: \"{}\"", sidecar_type,pid_file_path.display()); Ok(()) } -pub fn log_potential_stale_process(pid_file_path: PathBuf, pid: u32) { +pub fn log_potential_stale_process(pid_file_path: PathBuf, pid: u32, sidecar_type: SidecarType) { let mut system = System::new_all(); - let pid_u32 = pid; - let pid = Pid::from_u32(pid_u32); + let pid = Pid::from_u32(pid); system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); let Some(process) = system.process(pid) else { - warn!( - "Pid file {} was not created because the process was not found.", - pid_u32 + warn!(Source="Stale Process Cleanup"; + "{}: Pid file with process number '{}' was not created because the process was not found.", + sidecar_type, pid ); return; }; @@ -82,13 +77,13 @@ pub fn log_potential_stale_process(pid_file_path: PathBuf, pid: u32) { match File::create(&pid_file_path) { Ok(mut file) => { let name = process.name().to_string_lossy(); - let content = format!("{pid_u32}\n{name}\n"); + let content = format!("{pid}\n{name}\n"); if let Err(e) = file.write_all(content.as_bytes()) { - warn!("Failed to write to {}: {}", pid_file_path.display(), e); + warn!(Source="Stale Process Cleanup";"{}: Failed to write to \"{}\": {}", sidecar_type,pid_file_path.display(), e); } } Err(e) => { - warn!("Failed to create {}: {}", pid_file_path.display(), e); + warn!(Source="Stale Process Cleanup";"{}: Failed to create \"{}\": {}", sidecar_type, pid_file_path.display(), e); } } } From 2be5250c843016f2cd75bbc718f5af5f0cd52adc Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Tue, 3 Feb 2026 12:17:34 +0100 Subject: [PATCH 23/30] Changed I18N --- .../Assistants/I18N/allTexts.lua | 6 ++++++ .../plugin.lua | 15 +++++++++++++++ .../plugin.lua | 15 +++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 51f6e8ca7..dcc73b13c 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -5062,6 +5062,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is -- 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::INFORMATION::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." +-- This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1990446887"] = "This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes." + -- Copies the following to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard" @@ -5194,6 +5197,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3741877842"] = "AI Studio runs w -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements" +-- This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3860988493"] = "This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant." + -- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration." 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 cd9768f89..f290ae939 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 @@ -5022,6 +5022,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1282228996"] = "AI Studio läuft -- 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." @@ -5061,6 +5064,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek -- 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::INFORMATION::T1943216839"] = "Wir verwenden Rocket zur Implementierung der Runtime-API. Dies ist notwendig, da die Runtime mit der Benutzeroberfläche (IPC) kommunizieren muss. Rocket ist ein ausgezeichnetes Framework zur Umsetzung von Web-APIs in Rust." +-- This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1990446887"] = "Diese Bibliothek wird verwendet, um Prozesse über verschiedene Betriebssysteme hinweg zu verwalten und sicherzustellen, dass verwaiste oder Zombie-Prozesse ordnungsgemäß behandelt werden." + +-- Copies the following to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgendes in die Zwischenablage" + -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage" @@ -5190,6 +5199,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3741877842"] = "AI Studio läuft -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht" +-- This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3860988493"] = "Diese Bibliothek wird verwendet, um temporäre Ordner zum Speichern des Zertifikats und dessen privaten Schlüssels für die Kommunikation mit Qdrant zu erstellen." + -- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "Diese Bibliothek wird verwendet, um auf die Windows-Registry zuzugreifen. Wir nutzen sie in Windows-Unternehmensumgebungen, um die gewünschte Konfiguration auszulesen." @@ -5241,6 +5253,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "Wir verwenden die -- 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::INFORMATION::T870640199"] = "Für einige Datenübertragungen müssen wir die Daten in Base64 kodieren. Diese Rust-Bibliothek eignet sich dafür hervorragend." +-- 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::INFORMATION::T95576615"] = "Qdrant ist eine Vektordatenbank mit Vektorsuche. Sie ist produktionsreif mit einer benutzerfreundlichen API, um Punkte – Vektoren mit zusätzlichem Payload – zu speichern, zu durchsuchen und zu verwalten. Qdrant ist speziell auf erweiterte Filterfunktionen ausgelegt." + -- Install Pandoc UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Pandoc installieren" 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 d4fa1ba72..6a43ab749 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 @@ -5022,6 +5022,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1282228996"] = "AI Studio runs w -- 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." @@ -5061,6 +5064,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is -- 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::INFORMATION::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." +-- This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1990446887"] = "This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes." + +-- Copies the following to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard" + -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" @@ -5190,6 +5199,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3741877842"] = "AI Studio runs w -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements" +-- This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3860988493"] = "This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant." + -- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration." @@ -5241,6 +5253,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this libra -- 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::INFORMATION::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::INFORMATION::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::INFORMATION::T986578435"] = "Install Pandoc" From a00fa483a95d28a6d310edf0ce6fb1f7f5e2e790 Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Tue, 3 Feb 2026 12:18:23 +0100 Subject: [PATCH 24/30] improved build instruction guide --- documentation/Build.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/documentation/Build.md b/documentation/Build.md index 582270e05..600999fe4 100644 --- a/documentation/Build.md +++ b/documentation/Build.md @@ -52,12 +52,10 @@ You can now test your changes. To stop the application: > ⚠️ 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 restarting Tauri: -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. +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: From 6e6c4dd90fc19fa240a7f69941e2c1e3f9dcfb9c Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 3 Feb 2026 13:51:20 +0100 Subject: [PATCH 25/30] Added deep link to the licence --- app/MindWork AI Studio/Pages/Information.razor | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index cd97d4573..f1792e546 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -238,8 +238,8 @@ - - + + From 0c0ad3160ad92f4365f638851d3d9ae65b2b56cf Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 3 Feb 2026 14:07:01 +0100 Subject: [PATCH 26/30] Revised added libraries descriptions & updated I18N --- .../Assistants/I18N/allTexts.lua | 18 +++++++++--------- app/MindWork AI Studio/Pages/Information.razor | 6 +++--- .../plugin.lua | 18 +++++++++--------- .../plugin.lua | 18 +++++++++--------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index dcc73b13c..4fb3ec5dd 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -5026,6 +5026,9 @@ 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." +-- 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." @@ -5062,9 +5065,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is -- 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::INFORMATION::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." --- This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1990446887"] = "This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes." - -- Copies the following to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard" @@ -5113,6 +5113,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Used Open Source -- Build time UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time" +-- 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." @@ -5197,9 +5200,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3741877842"] = "AI Studio runs w -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements" --- This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3860988493"] = "This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant." - -- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration." @@ -5233,6 +5233,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library -- Used .NET SDK UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK" +-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated." + -- Did you find a bug or are you experiencing issues? Report your concern here. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Did you find a bug or are you experiencing issues? Report your concern here." @@ -5251,9 +5254,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this libra -- 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::INFORMATION::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::INFORMATION::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::INFORMATION::T986578435"] = "Install Pandoc" diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index f1792e546..726739820 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -217,7 +217,7 @@ - + @@ -238,8 +238,8 @@ - - + + 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 f290ae939..e5382f0a3 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 @@ -5028,6 +5028,9 @@ 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." +-- 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." @@ -5064,9 +5067,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek -- 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::INFORMATION::T1943216839"] = "Wir verwenden Rocket zur Implementierung der Runtime-API. Dies ist notwendig, da die Runtime mit der Benutzeroberfläche (IPC) kommunizieren muss. Rocket ist ein ausgezeichnetes Framework zur Umsetzung von Web-APIs in Rust." --- This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1990446887"] = "Diese Bibliothek wird verwendet, um Prozesse über verschiedene Betriebssysteme hinweg zu verwalten und sicherzustellen, dass verwaiste oder Zombie-Prozesse ordnungsgemäß behandelt werden." - -- Copies the following to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgendes in die Zwischenablage" @@ -5115,6 +5115,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Verwendete Open- -- Build time UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build-Zeit" +-- 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." @@ -5199,9 +5202,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3741877842"] = "AI Studio läuft -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht" --- This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3860988493"] = "Diese Bibliothek wird verwendet, um temporäre Ordner zum Speichern des Zertifikats und dessen privaten Schlüssels für die Kommunikation mit Qdrant zu erstellen." - -- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "Diese Bibliothek wird verwendet, um auf die Windows-Registry zuzugreifen. Wir nutzen sie in Windows-Unternehmensumgebungen, um die gewünschte Konfiguration auszulesen." @@ -5235,6 +5235,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "Dies ist eine Bib -- Used .NET SDK UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Verwendetes .NET SDK" +-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "Diese Bibliothek wird verwendet, um Sidecar-Prozesse zu verwalten und sicherzustellen, dass veraltete oder Zombie-Sidecars erkannt und beendet werden." + -- Did you find a bug or are you experiencing issues? Report your concern here. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Haben Sie einen Fehler gefunden oder Probleme festgestellt? Melden Sie Ihr Anliegen hier." @@ -5253,9 +5256,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "Wir verwenden die -- 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::INFORMATION::T870640199"] = "Für einige Datenübertragungen müssen wir die Daten in Base64 kodieren. Diese Rust-Bibliothek eignet sich dafür hervorragend." --- 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::INFORMATION::T95576615"] = "Qdrant ist eine Vektordatenbank mit Vektorsuche. Sie ist produktionsreif mit einer benutzerfreundlichen API, um Punkte – Vektoren mit zusätzlichem Payload – zu speichern, zu durchsuchen und zu verwalten. Qdrant ist speziell auf erweiterte Filterfunktionen ausgelegt." - -- Install Pandoc UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T986578435"] = "Pandoc installieren" 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 6a43ab749..45c00ccb8 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 @@ -5028,6 +5028,9 @@ 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." +-- 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." @@ -5064,9 +5067,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is -- 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::INFORMATION::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." --- This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1990446887"] = "This library is used to manage processes across different operating systems, ensuring proper handling of stale or zombie processes." - -- Copies the following to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard" @@ -5115,6 +5115,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2557066213"] = "Used Open Source -- Build time UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T260228112"] = "Build time" +-- 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." @@ -5199,9 +5202,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3741877842"] = "AI Studio runs w -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements" --- This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3860988493"] = "This library is used to create temporary folders for saving the certificate and private key data for communication with Qdrant." - -- This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3874337003"] = "This library is used to access the Windows registry. We use this for Windows enterprise environments to read the desired configuration." @@ -5235,6 +5235,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library -- Used .NET SDK UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK" +-- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated." + -- Did you find a bug or are you experiencing issues? Report your concern here. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T639371534"] = "Did you find a bug or are you experiencing issues? Report your concern here." @@ -5253,9 +5256,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this libra -- 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::INFORMATION::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::INFORMATION::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::INFORMATION::T986578435"] = "Install Pandoc" From 631092f2a944f1c094144d088a47eef18b11f423 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 3 Feb 2026 14:20:46 +0100 Subject: [PATCH 27/30] Formatting --- app/MindWork AI Studio/Program.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index c522d8011..85e97b07e 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -91,6 +91,7 @@ public static async Task Main() 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."); From f0b4cf290e65638bd5cdc608bc64966b0a258d4a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 3 Feb 2026 14:28:39 +0100 Subject: [PATCH 28/30] Updated changelog --- app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md index 23c3747cd..e4b9137be 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.2.2.md @@ -1 +1,2 @@ # v26.2.2, build 234 (2026-02-xx xx:xx UTC) +- Added a vector database (Qdrant) as a building block for our local RAG (retrieval-augmented generation) solution. Thank you very much, Paul (`PaulKoudelka`), for this major contribution. Note that our local RAG implementation remained in preview and has not yet been released; other building blocks are not yet ready. \ No newline at end of file From f869101180feaa36f31baf29b71e881057c2bd86 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 3 Feb 2026 14:31:12 +0100 Subject: [PATCH 29/30] Updated RAG work plan --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30536bcd4..363ec8aa5 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 [Qdrant](https://github.com/qdrant/qdrant) +- [x] ~~Runtime: Integration of the vector database [Qdrant](https://github.com/qdrant/qdrant) ([PR #580](https://github.com/MindWorkAI/AI-Studio/pull/580))~~ - [ ] 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))~~ From 8fe29ad5992a884a23212836e37c8019bceaff44 Mon Sep 17 00:00:00 2001 From: PaulKoudelka Date: Thu, 5 Feb 2026 17:27:11 +0100 Subject: [PATCH 30/30] added API for embedding providers --- .../Assistants/I18N/allTexts.lua | 24 ++++++ .../Settings/SettingsPanelEmbeddings.razor | 3 + .../Settings/SettingsPanelEmbeddings.razor.cs | 48 ++++++++++- .../Dialogs/EmbeddingResultDialog.razor | 22 +++++ .../Dialogs/EmbeddingResultDialog.razor.cs | 21 +++++ .../plugin.lua | 26 +++++- .../plugin.lua | 26 +++++- .../AlibabaCloud/ProviderAlibabaCloud.cs | 7 ++ .../Provider/Anthropic/ProviderAnthropic.cs | 6 ++ .../Provider/BaseProvider.cs | 80 ++++++++++++++++++ .../Provider/DeepSeek/ProviderDeepSeek.cs | 6 ++ .../Provider/EmbeddingResponse.cs | 24 ++++++ .../Provider/Fireworks/ProviderFireworks.cs | 6 ++ .../Provider/GWDG/ProviderGWDG.cs | 6 ++ .../Provider/Google/ProviderGoogle.cs | 84 ++++++++++++++++++- .../Provider/Groq/ProviderGroq.cs | 6 ++ .../Provider/Helmholtz/ProviderHelmholtz.cs | 7 ++ .../HuggingFace/ProviderHuggingFace.cs | 6 ++ app/MindWork AI Studio/Provider/IProvider.cs | 10 +++ .../Provider/Mistral/ProviderMistral.cs | 7 ++ app/MindWork AI Studio/Provider/NoProvider.cs | 2 + .../Provider/OpenAI/ProviderOpenAI.cs | 7 ++ .../Provider/OpenRouter/ProviderOpenRouter.cs | 7 ++ .../Provider/Perplexity/ProviderPerplexity.cs | 6 ++ .../Provider/SelfHosted/HostExtensions.cs | 5 ++ .../Provider/SelfHosted/ProviderSelfHosted.cs | 7 ++ .../Provider/X/ProviderX.cs | 6 ++ 27 files changed, 461 insertions(+), 4 deletions(-) create mode 100644 app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor.cs create mode 100644 app/MindWork AI Studio/Provider/EmbeddingResponse.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index f825d2298..d8658ebe2 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2140,9 +2140,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] -- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence." +-- Embedding Result +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result" + -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete" +-- Test Embedding Provider +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Test Embedding Provider" + -- Add Embedding UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding" @@ -2155,6 +2161,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253 -- Add Embedding Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider" +-- Embedding Vector (one dimension per line) +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Embedding Vector (one dimension per line)" + -- Model UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model" @@ -2164,6 +2173,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199 -- Name UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name" +-- No embedding was returned. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "No embedding was returned." + -- Configured Embedding Providers UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers" @@ -2173,6 +2185,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512 -- Edit UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit" +-- Close +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Close" + -- Actions UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions" @@ -2194,6 +2209,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130 -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard" +-- Test +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Test" + -- Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider" @@ -3274,6 +3292,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Pro -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel" +-- Embedding Vector +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embedding Vector" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system." diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor index 874bc3c90..958abd8ff 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor @@ -56,6 +56,9 @@ + + + } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs index 948789874..cc9ef6941 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs @@ -1,4 +1,6 @@ +using System.Globalization; using AIStudio.Dialogs; +using AIStudio.Provider; using AIStudio.Settings; using Microsoft.AspNetCore.Components; @@ -123,4 +125,48 @@ private async Task UpdateEmbeddingProviders() await this.AvailableEmbeddingProvidersChanged.InvokeAsync(this.AvailableEmbeddingProviders); } -} \ No newline at end of file + + private async Task TestEmbeddingProvider(EmbeddingProvider provider) + { + var dialogParameters = new DialogParameters + { + { x => x.ConfirmText, "Embed text" }, + { x => x.InputHeaderText, "Add text that should be embedded:" }, + { x => x.UserInput, "Add text here"} + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Test Embedding Provider"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var inputText = dialogResult.Data as string; + if (string.IsNullOrWhiteSpace(inputText)) + return; + + var embeddingProvider = provider.CreateProvider(); + var embeddings = await embeddingProvider.EmbedTextAsync(provider.Model, this.SettingsManager, default, new List { inputText }); + + if (embeddings.Count == 0) + { + await this.DialogService.ShowMessageBox(T("Embedding Result"), T("No embedding was returned."), T("Close")); + return; + } + + var vector = embeddings.FirstOrDefault(); + if (vector is null || vector.Count == 0) + { + await this.DialogService.ShowMessageBox(T("Embedding Result"), T("No embedding was returned."), T("Close")); + return; + } + + var resultText = string.Join(Environment.NewLine, vector.Select(value => value.ToString("G9", CultureInfo.InvariantCulture))); + var resultParameters = new DialogParameters + { + { x => x.ResultText, resultText }, + { x => x.ResultLabel, T("Embedding Vector (one dimension per line)") }, + }; + + await this.DialogService.ShowAsync(T("Embedding Result"), resultParameters, DialogOptions.FULLSCREEN); + } +} diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor new file mode 100644 index 000000000..8e1408ef3 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor @@ -0,0 +1,22 @@ +@inherits MSGComponentBase + + + + + + + + @T("Close") + + + diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor.cs new file mode 100644 index 000000000..96830edf3 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/EmbeddingResultDialog.razor.cs @@ -0,0 +1,21 @@ +using AIStudio.Components; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class EmbeddingResultDialog : MSGComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public string ResultText { get; set; } = string.Empty; + + [Parameter] + public string ResultLabel { get; set; } = string.Empty; + + private string ResultLabelText => string.IsNullOrWhiteSpace(this.ResultLabel) ? T("Embedding Vector") : this.ResultLabel; + + private void Close() => this.MudDialog.Close(); +} 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 5f4adf5db..87ef39c40 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 @@ -2142,9 +2142,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] -- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Möchten Sie einen Anbieter als Standard für die gesamte App festlegen? Wenn Sie einen anderen Anbieter für einen Assistenten konfigurieren, hat dieser immer Vorrang." +-- Embedding Result +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Einbettungsergebnis" + -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Löschen" +-- Test Embedding Provider +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Embedding-Anbieter testen" + -- Add Embedding UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Einbettung hinzufügen" @@ -2157,6 +2163,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253 -- Add Embedding Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Einbettungsanbieter hinzufügen" +-- Embedding Vector (one dimension per line) +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Einbettungsvektor (eine Dimension pro Zeile)" + -- Model UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Modell" @@ -2166,6 +2175,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199 -- Name UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name" +-- No embedding was returned. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "Es wurde keine Einbettung zurückgegeben." + -- Configured Embedding Providers UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Konfigurierte Anbieter für Einbettungen" @@ -2175,6 +2187,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512 -- Edit UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Bearbeiten" +-- Close +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Schließen" + -- Actions UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Aktionen" @@ -2196,6 +2211,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130 -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Dashboard öffnen" +-- Test +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Testen" + -- Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Anbieter" @@ -3276,6 +3294,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Anb -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Abbrechen" +-- Embedding Vector +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Einbettungsvektor" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Schließen" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Leider ist die GPL-Lizenz von Pandoc nicht mit der Lizenz von AI Studio kompatibel. Software unter der GPL-Lizenz ist jedoch kostenlos und frei nutzbar. Sie müssen die GPL-Lizenz akzeptieren, bevor wir Pandoc automatisch für Sie herunterladen und installieren können (empfohlen). Alternativ können Sie Pandoc auch selbst herunterladen – entweder mit den untenstehenden Anweisungen oder auf anderem Weg, zum Beispiel über den Paketmanager Ihres Betriebssystems." @@ -5019,7 +5043,7 @@ 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." --- 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. +-- 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. 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 5a4f9f787..ae4d097b8 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 @@ -2142,9 +2142,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T817101267"] -- Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T844514734"] = "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence." +-- Embedding Result +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result" + -- Delete UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete" +-- Test Embedding Provider +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1655784761"] = "Test Embedding Provider" + -- Add Embedding UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1738753945"] = "Add Embedding" @@ -2157,6 +2163,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T18253 -- Add Embedding Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T190634634"] = "Add Embedding Provider" +-- Embedding Vector (one dimension per line) +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2174876961"] = "Embedding Vector (one dimension per line)" + -- Model UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model" @@ -2166,6 +2175,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T24199 -- Name UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T266367750"] = "Name" +-- No embedding was returned. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T291969"] = "No embedding was returned." + -- Configured Embedding Providers UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T305753126"] = "Configured Embedding Providers" @@ -2175,6 +2187,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T32512 -- Edit UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3267849393"] = "Edit" +-- Close +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3448155331"] = "Close" + -- Actions UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T3865031940"] = "Actions" @@ -2196,6 +2211,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T51130 -- Open Dashboard UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T78223861"] = "Open Dashboard" +-- Test +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T805092869"] = "Test" + -- Provider UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T900237532"] = "Provider" @@ -3276,6 +3294,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900237532"] = "Pro -- Cancel UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGPROVIDERDIALOG::T900713019"] = "Cancel" +-- Embedding Vector +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T1173984541"] = "Embedding Vector" + +-- Close +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::EMBEDDINGRESULTDIALOG::T3448155331"] = "Close" + -- Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T1001483402"] = "Unfortunately, Pandoc's GPL license isn't compatible with the AI Studios licenses. However, software under the GPL is free to use and free of charge. You'll need to accept the GPL license before we can download and install Pandoc for you automatically (recommended). Alternatively, you might download it yourself using the instructions below or install it otherwise, e.g., by using a package manager of your operating system." @@ -5019,7 +5043,7 @@ 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." --- 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. +-- 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. diff --git a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs index de46e95b1..d39351b2f 100644 --- a/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs +++ b/app/MindWork AI Studio/Provider/AlibabaCloud/ProviderAlibabaCloud.cs @@ -86,6 +86,13 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs index b536ee4db..533670ad7 100644 --- a/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs +++ b/app/MindWork AI Studio/Provider/Anthropic/ProviderAnthropic.cs @@ -113,6 +113,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/BaseProvider.cs b/app/MindWork AI Studio/Provider/BaseProvider.cs index 8400e9a32..76108745d 100644 --- a/app/MindWork AI Studio/Provider/BaseProvider.cs +++ b/app/MindWork AI Studio/Provider/BaseProvider.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Http.Headers; using System.Runtime.CompilerServices; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -98,6 +99,9 @@ protected BaseProvider(LLMProviders provider, string url, ILogger logger) /// public abstract Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default); + /// + public abstract Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts); + /// public abstract Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default); @@ -640,6 +644,82 @@ protected async Task PerformStandardTranscriptionRequest(RequestedSecret } } + protected async Task>> PerformStandardTextEmbeddingRequest(RequestedSecret requestedSecret, Model embeddingModel, Host host = Host.NONE, CancellationToken token = default, params List texts) + { + try + { + // + // Add the model name to the form data. Ensure that a model name is always provided. + // Otherwise, the StringContent constructor will throw an exception. + // + var modelName = embeddingModel.Id; + if (string.IsNullOrWhiteSpace(modelName)) + modelName = "placeholder"; + + // Prepare the HTTP embedding request: + var payload = new + { + model = modelName, + input = texts, + encoding_format = "float" + }; + var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS); + + using var request = new HttpRequestMessage(HttpMethod.Post, host.EmbeddingURL()); + + // Handle the authorization header based on the provider: + switch (this.Provider) + { + case LLMProviders.SELF_HOSTED: + if(requestedSecret.Success) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + break; + + default: + if(!requestedSecret.Success) + { + this.logger.LogError("No valid API key available for embedding request."); + return Array.Empty>(); + } + + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + break; + } + + // Set the content: + request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json"); + + using var response = await this.httpClient.SendAsync(request, token); + var responseBody = response.Content.ReadAsStringAsync(token).Result; + + if (!response.IsSuccessStatusCode) + { + this.logger.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody); + return Array.Empty>(); + } + + var embeddingResponse = JsonSerializer.Deserialize(responseBody, JSON_SERIALIZER_OPTIONS); + if (embeddingResponse is { Data: not null }) + { + return embeddingResponse.Data + .Select(d => d.Embedding?.ToArray() ?? Array.Empty()) + .Cast>() + .ToArray(); + } + else + { + this.logger.LogError("Was not able to deserialize the embedding response."); + return Array.Empty>(); + } + } + catch (Exception e) + { + this.logger.LogError("Failed to perform embedding request: '{Message}'.", e.Message); + return Array.Empty>(); + } + } + /// /// Parse and convert API parameters from a provided JSON string into a dictionary, /// optionally merging additional parameters and removing specific keys. diff --git a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs index ce33f2886..ce86ef68b 100644 --- a/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs +++ b/app/MindWork AI Studio/Provider/DeepSeek/ProviderDeepSeek.cs @@ -86,6 +86,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/EmbeddingResponse.cs b/app/MindWork AI Studio/Provider/EmbeddingResponse.cs new file mode 100644 index 000000000..89cf5fb8d --- /dev/null +++ b/app/MindWork AI Studio/Provider/EmbeddingResponse.cs @@ -0,0 +1,24 @@ +namespace AIStudio.Provider; + +public sealed record EmbeddingResponse +{ + public string? Id { get; set; } + public string? Object { get; set; } + public List? Data { get; set; } + public string? Model { get; set; } + public Usage? Usage { get; set; } +} + +public sealed record EmbeddingData +{ + public string? Object { get; set; } + public List? Embedding { get; set; } + public int? Index { get; set; } +} + +public sealed record Usage +{ + public int? PromptTokens { get; set; } + public int? TotalTokens { get; set; } + public int? CompletionTokens { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs index 1eb21894d..cd7a5abd7 100644 --- a/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs +++ b/app/MindWork AI Studio/Provider/Fireworks/ProviderFireworks.cs @@ -88,6 +88,12 @@ public override async Task TranscribeAudioAsync(Model transcriptionModel var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs index 2b7e4dcb0..784faa457 100644 --- a/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs +++ b/app/MindWork AI Studio/Provider/GWDG/ProviderGWDG.cs @@ -87,6 +87,12 @@ public override async Task TranscribeAudioAsync(Model transcriptionModel var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs index 48dea49e9..224fe5e6c 100644 --- a/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs +++ b/app/MindWork AI Studio/Provider/Google/ProviderGoogle.cs @@ -87,6 +87,78 @@ public override Task TranscribeAudioAsync(Provider.Model transcriptionMo { return Task.FromResult(string.Empty); } + + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + try + { + var modelName = embeddingModel.Id; + if (string.IsNullOrWhiteSpace(modelName)) + { + LOGGER.LogError("No model name provided for embedding request."); + return Array.Empty>(); + } + + if (modelName.StartsWith("models/", StringComparison.OrdinalIgnoreCase)) + modelName = modelName.Substring("models/".Length); + + if (!requestedSecret.Success) + { + LOGGER.LogError("No valid API key available for embedding request."); + return Array.Empty>(); + } + + // Prepare the Google Gemini embedding request: + var payload = new + { + content = new + { + parts = texts.Select(text => new { text }).ToArray() + }, + taskType = "SEMANTIC_SIMILARITY" + }; + var embeddingRequest = JsonSerializer.Serialize(payload, JSON_SERIALIZER_OPTIONS); + + var embedUrl = $"https://generativelanguage.googleapis.com/v1beta/models/{modelName}:embedContent"; + using var request = new HttpRequestMessage(HttpMethod.Post, embedUrl); + request.Headers.Add("x-goog-api-key", await requestedSecret.Secret.Decrypt(ENCRYPTION)); + + // Set the content: + request.Content = new StringContent(embeddingRequest, Encoding.UTF8, "application/json"); + + using var response = await this.httpClient.SendAsync(request, token); + var responseBody = await response.Content.ReadAsStringAsync(token); + + if (!response.IsSuccessStatusCode) + { + LOGGER.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody); + return Array.Empty>(); + } + + var embeddingResponse = JsonSerializer.Deserialize(responseBody, JSON_SERIALIZER_OPTIONS); + + if (embeddingResponse is { Embedding: not null }) + { + return embeddingResponse.Embedding + .Select(d => d.Values?.ToArray() ?? Array.Empty()) + .Cast>() + .ToArray(); + } + else + { + LOGGER.LogError("Was not able to deserialize the embedding response."); + return Array.Empty>(); + } + + } + catch (Exception e) + { + LOGGER.LogError("Failed to perform embedding request: '{Message}'.", e.Message); + return Array.Empty>(); + } + } /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) @@ -150,4 +222,14 @@ private async Task LoadModels(SecretStoreType storeType, Cancell var modelResponse = await response.Content.ReadFromJsonAsync(token); return modelResponse; } -} \ No newline at end of file + + private sealed record GoogleEmbeddingResponse + { + public List? Embedding { get; set; } + } + + private sealed record GoogleEmbedding + { + public List? Values { get; set; } + } +} diff --git a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs index 07cdb3900..1802150b9 100644 --- a/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs +++ b/app/MindWork AI Studio/Provider/Groq/ProviderGroq.cs @@ -87,6 +87,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs index ec5fca2c3..3df909c08 100644 --- a/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs +++ b/app/MindWork AI Studio/Provider/Helmholtz/ProviderHelmholtz.cs @@ -86,6 +86,13 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs index a05ca11e2..47db58e8e 100644 --- a/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs +++ b/app/MindWork AI Studio/Provider/HuggingFace/ProviderHuggingFace.cs @@ -91,6 +91,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/IProvider.cs b/app/MindWork AI Studio/Provider/IProvider.cs index 5c3900746..ef15dd21b 100644 --- a/app/MindWork AI Studio/Provider/IProvider.cs +++ b/app/MindWork AI Studio/Provider/IProvider.cs @@ -59,6 +59,16 @@ public interface IProvider /// The cancellation token. /// >The transcription result. public Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default); + + /// + /// Embed a text file. + /// + /// The model to use for embedding. + /// The settings manager instance to use. + /// The cancellation token. + /// /// A single string or a list of strings to embed. + /// >The embedded text as a single vector or as a list of vectors. + public Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts); /// /// Load all possible text models that can be used with this provider. diff --git a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs index 6685e6d6e..f4cb07f4a 100644 --- a/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs +++ b/app/MindWork AI Studio/Provider/Mistral/ProviderMistral.cs @@ -89,6 +89,13 @@ public override async Task TranscribeAudioAsync(Provider.Model transcrip return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } + /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) { diff --git a/app/MindWork AI Studio/Provider/NoProvider.cs b/app/MindWork AI Studio/Provider/NoProvider.cs index a650ac343..cb24e0faa 100644 --- a/app/MindWork AI Studio/Provider/NoProvider.cs +++ b/app/MindWork AI Studio/Provider/NoProvider.cs @@ -40,6 +40,8 @@ public async IAsyncEnumerable StreamImageCompletion(Model imageModel, public Task TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) => Task.FromResult(string.Empty); + public Task>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) => Task.FromResult>>(Array.Empty>()); + public IReadOnlyCollection GetModelCapabilities(Model model) => [ Capability.NONE ]; #endregion diff --git a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs index d2d0b32b5..90194ed77 100644 --- a/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs +++ b/app/MindWork AI Studio/Provider/OpenAI/ProviderOpenAI.cs @@ -224,6 +224,13 @@ public override async Task TranscribeAudioAsync(Model transcriptionModel var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER); return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token); } + + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs index ca8ef1554..444703c5a 100644 --- a/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs +++ b/app/MindWork AI Studio/Provider/OpenRouter/ProviderOpenRouter.cs @@ -94,6 +94,13 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs index 691dcdd54..804c8334a 100644 --- a/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs +++ b/app/MindWork AI Studio/Provider/Perplexity/ProviderPerplexity.cs @@ -94,6 +94,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri { return Task.FromResult(string.Empty); } + + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } /// public override Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) diff --git a/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs b/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs index 6c475273e..25dc07ca1 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/HostExtensions.cs @@ -30,6 +30,11 @@ public static class HostExtensions _ => "audio/transcriptions", }; + public static string EmbeddingURL(this Host host) => host switch + { + _ => "embeddings", + }; + public static bool IsChatSupported(this Host host) { switch (host) diff --git a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs index a1e411e18..9b3d6d674 100644 --- a/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs +++ b/app/MindWork AI Studio/Provider/SelfHosted/ProviderSelfHosted.cs @@ -95,6 +95,13 @@ public override async Task TranscribeAudioAsync(Provider.Model transcrip return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, host, token); } + /// + public override async Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.EMBEDDING_PROVIDER); + return await this.PerformStandardTextEmbeddingRequest(requestedSecret, embeddingModel, token: token, texts: texts); + } + public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) { try diff --git a/app/MindWork AI Studio/Provider/X/ProviderX.cs b/app/MindWork AI Studio/Provider/X/ProviderX.cs index a0510dd6a..22abaef02 100644 --- a/app/MindWork AI Studio/Provider/X/ProviderX.cs +++ b/app/MindWork AI Studio/Provider/X/ProviderX.cs @@ -88,6 +88,12 @@ public override Task TranscribeAudioAsync(Model transcriptionModel, stri return Task.FromResult(string.Empty); } + /// + public override Task>> EmbedTextAsync(Provider.Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List texts) + { + return Task.FromResult>>(Array.Empty>()); + } + /// public override async Task> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) {