diff --git a/app/MindWork AI Studio/Dialogs/PandocDialog.razor.cs b/app/MindWork AI Studio/Dialogs/PandocDialog.razor.cs index d108f669..7f35f6f7 100644 --- a/app/MindWork AI Studio/Dialogs/PandocDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/PandocDialog.razor.cs @@ -1,7 +1,4 @@ -using System.Reflection; - -using AIStudio.Components; -using AIStudio.Tools.Metadata; +using AIStudio.Components; using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; @@ -11,9 +8,8 @@ namespace AIStudio.Dialogs; public partial class PandocDialog : MSGComponentBase { - private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly(); - private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute()!; - private static readonly RID CPU_ARCHITECTURE = META_DATA_ARCH.Architecture.ToRID(); + // Use runtime detection instead of metadata to ensure correct RID on dev machines: + private static readonly RID CPU_ARCHITECTURE = RIDExtensions.GetCurrentRID(); [Parameter] public bool ShowInstallationPage { get; set; } diff --git a/app/MindWork AI Studio/Tools/Pandoc.cs b/app/MindWork AI Studio/Tools/Pandoc.cs index afce67f9..ef6b9deb 100644 --- a/app/MindWork AI Studio/Tools/Pandoc.cs +++ b/app/MindWork AI Studio/Tools/Pandoc.cs @@ -14,18 +14,26 @@ namespace AIStudio.Tools; public static partial class Pandoc { private static string TB(string fallbackEN) => PluginSystem.I18N.I.T(fallbackEN, typeof(Pandoc).Namespace, nameof(Pandoc)); - + private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly(); private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute()!; - private static readonly RID CPU_ARCHITECTURE = META_DATA_ARCH.Architecture.ToRID(); - + + // Use runtime detection instead of metadata to ensure correct RID on dev machines: + private static readonly RID CPU_ARCHITECTURE = RIDExtensions.GetCurrentRID(); + private static readonly RID METADATA_ARCHITECTURE = META_DATA_ARCH.Architecture.ToRID(); + private const string DOWNLOAD_URL = "https://github.com/jgm/pandoc/releases/download"; private const string LATEST_URL = "https://github.com/jgm/pandoc/releases/latest"; - + private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(Pandoc)); private static readonly Version MINIMUM_REQUIRED_VERSION = new (3, 7, 0, 2); private static readonly Version FALLBACK_VERSION = new (3, 7, 0, 2); + /// + /// Tracks whether the first availability check log has been written to avoid log spam on repeated calls. + /// + private static bool HAS_LOGGED_AVAILABILITY_CHECK_ONCE; + /// /// Prepares a Pandoc process by using the Pandoc process builder. /// @@ -41,16 +49,39 @@ public static partial class Pandoc /// True, if pandoc is available and the minimum required version is met, else false. public static async Task CheckAvailabilityAsync(RustService rustService, bool showMessages = true, bool showSuccessMessage = true) { + // + // Determine if we should log (only on the first call): + // + var shouldLog = !HAS_LOGGED_AVAILABILITY_CHECK_ONCE; + try { + // + // Log a warning if the runtime-detected RID differs from the metadata RID. + // This can happen on dev machines where the metadata.txt contains stale values. + // We always use the runtime-detected RID for correct behavior. + // + if (shouldLog && CPU_ARCHITECTURE != METADATA_ARCHITECTURE) + { + LOG.LogWarning( + "Runtime-detected RID '{RuntimeRID}' differs from metadata RID '{MetadataRID}'. Using runtime-detected RID. This is expected on dev machines where metadata.txt may be outdated.", + CPU_ARCHITECTURE.ToUserFriendlyName(), + METADATA_ARCHITECTURE.ToUserFriendlyName()); + } + var preparedProcess = await PreparePandocProcess().AddArgument("--version").BuildAsync(rustService); + if (shouldLog) + LOG.LogInformation("Checking Pandoc availability using executable: '{Executable}' (IsLocal: {IsLocal}).", preparedProcess.StartInfo.FileName, preparedProcess.IsLocal); + using var process = Process.Start(preparedProcess.StartInfo); if (process == null) { if (showMessages) - await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Help, TB("Was not able to check the Pandoc installation."))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Help, TB("Was not able to check the Pandoc installation."))); - LOG.LogInformation("The Pandoc process was not started, it was null"); + if (shouldLog) + LOG.LogError("The Pandoc process was not started, it was null. Executable path: '{Executable}'.", preparedProcess.StartInfo.FileName); + return new(false, TB("Was not able to check the Pandoc installation."), false, string.Empty, preparedProcess.IsLocal); } @@ -66,9 +97,11 @@ public static partial class Pandoc if (process.ExitCode != 0) { if (showMessages) - await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("Pandoc is not available on the system or the process had issues."))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc is not available on the system or the process had issues."))); - LOG.LogError("The Pandoc process exited with code {ProcessExitCode}. Error output: '{ErrorText}'", process.ExitCode, error); + if (shouldLog) + LOG.LogError("The Pandoc process exited with code {ProcessExitCode}. Error output: '{ErrorText}'", process.ExitCode, error); + return new(false, TB("Pandoc is not available on the system or the process had issues."), false, string.Empty, preparedProcess.IsLocal); } @@ -76,39 +109,51 @@ public static partial class Pandoc if (!versionMatch.Success) { if (showMessages) - await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Terminal, TB("Was not able to validate the Pandoc installation."))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Terminal, TB("Was not able to validate the Pandoc installation."))); + + if (shouldLog) + LOG.LogError("Pandoc --version returned an invalid format: '{Output}'.", output); - LOG.LogError("Pandoc --version returned an invalid format: {Output}", output); return new(false, TB("Was not able to validate the Pandoc installation."), false, string.Empty, preparedProcess.IsLocal); } - + var versions = versionMatch.Groups[1].Value; var installedVersion = Version.Parse(versions); var installedVersionString = installedVersion.ToString(); - + if (installedVersion >= MINIMUM_REQUIRED_VERSION) { if (showMessages && showSuccessMessage) await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, string.Format(TB("Pandoc v{0} is installed."), installedVersionString))); - - LOG.LogInformation("Pandoc v{0} is installed and matches the required version (v{1})", installedVersionString, MINIMUM_REQUIRED_VERSION.ToString()); + + if (shouldLog) + LOG.LogInformation("Pandoc v{0} is installed and matches the required version (v{1}).", installedVersionString, MINIMUM_REQUIRED_VERSION.ToString()); + return new(true, string.Empty, true, installedVersionString, preparedProcess.IsLocal); } - + if (showMessages) - await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Build, string.Format(TB("Pandoc v{0} is installed, but it doesn't match the required version (v{1})."), installedVersionString, MINIMUM_REQUIRED_VERSION.ToString()))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Build, string.Format(TB("Pandoc v{0} is installed, but it doesn't match the required version (v{1})."), installedVersionString, MINIMUM_REQUIRED_VERSION.ToString()))); + + if (shouldLog) + LOG.LogWarning("Pandoc v{0} is installed, but it does not match the required version (v{1}).", installedVersionString, MINIMUM_REQUIRED_VERSION.ToString()); - LOG.LogWarning("Pandoc v{0} is installed, but it does not match the required version (v{1})", installedVersionString, MINIMUM_REQUIRED_VERSION.ToString()); return new(true, string.Format(TB("Pandoc v{0} is installed, but it does not match the required version (v{1})."), installedVersionString, MINIMUM_REQUIRED_VERSION.ToString()), false, installedVersionString, preparedProcess.IsLocal); } catch (Exception e) { if (showMessages) - await MessageBus.INSTANCE.SendError(new (@Icons.Material.Filled.AppsOutage, TB("It seems that Pandoc is not installed."))); + await MessageBus.INSTANCE.SendError(new(@Icons.Material.Filled.AppsOutage, TB("It seems that Pandoc is not installed."))); + + if(shouldLog) + LOG.LogError(e, "Pandoc availability check failed. This usually means Pandoc is not installed or not in the system PATH."); - LOG.LogError("Pandoc is not installed and threw an exception: {0}", e.Message); return new(false, TB("It seems that Pandoc is not installed."), false, string.Empty, false); } + finally + { + HAS_LOGGED_AVAILABILITY_CHECK_ONCE = true; + } } /// diff --git a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs index 90bfb468..c2c404a7 100644 --- a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs +++ b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs @@ -13,8 +13,15 @@ public sealed class PandocProcessBuilder { private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly(); private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute()!; - private static readonly RID CPU_ARCHITECTURE = META_DATA_ARCH.Architecture.ToRID(); - + + // Use runtime detection instead of metadata to ensure correct RID on dev machines: + private static readonly RID CPU_ARCHITECTURE = RIDExtensions.GetCurrentRID(); + private static readonly RID METADATA_ARCHITECTURE = META_DATA_ARCH.Architecture.ToRID(); + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PandocProcessBuilder)); + + // Tracks whether the first log has been written to avoid log spam on repeated calls: + private static bool HAS_LOGGED_ONCE; + private string? providedInputFile; private string? providedOutputFile; private string? providedInputFormat; @@ -112,30 +119,107 @@ public sealed class PandocProcessBuilder private static async Task PandocExecutablePath(RustService rustService) { // - // First, we try to find the pandoc executable in the data directory. - // Any local installation should be preferred over the system-wide installation. + // Determine if we should log (only on the first call): // - var localInstallationRootDirectory = await Pandoc.GetPandocDataFolder(rustService); + var shouldLog = !HAS_LOGGED_ONCE; + try { - var executableName = PandocExecutableName; - var subdirectories = Directory.GetDirectories(localInstallationRootDirectory, "*", SearchOption.AllDirectories); - foreach (var subdirectory in subdirectories) + // + // Log a warning if the runtime-detected RID differs from the metadata RID. + // This can happen on dev machines where the metadata.txt contains stale values. + // We always use the runtime-detected RID for correct behavior. + // + if (shouldLog && CPU_ARCHITECTURE != METADATA_ARCHITECTURE) { - var pandocPath = Path.Combine(subdirectory, executableName); - if (File.Exists(pandocPath)) - return new(pandocPath, true); + LOGGER.LogWarning( + "Runtime-detected RID '{RuntimeRID}' differs from metadata RID '{MetadataRID}'. Using runtime-detected RID. This is expected on dev machines where metadata.txt may be outdated.", + CPU_ARCHITECTURE.ToUserFriendlyName(), + METADATA_ARCHITECTURE.ToUserFriendlyName()); } + + // + // First, we try to find the pandoc executable in the data directory. + // Any local installation should be preferred over the system-wide installation. + // + var localInstallationRootDirectory = await Pandoc.GetPandocDataFolder(rustService); + + // + // Check if the data directory path is valid: + // + if (string.IsNullOrWhiteSpace(localInstallationRootDirectory)) + { + if (shouldLog) + LOGGER.LogWarning("The local data directory path is empty or null. Cannot search for local Pandoc installation."); + } + else if (!Directory.Exists(localInstallationRootDirectory)) + { + if (shouldLog) + LOGGER.LogWarning("The local Pandoc installation directory does not exist: '{LocalInstallationRootDirectory}'.", localInstallationRootDirectory); + } + else + { + // + // The directory exists, search for the pandoc executable: + // + var executableName = PandocExecutableName; + if (shouldLog) + LOGGER.LogInformation("Searching for Pandoc executable '{ExecutableName}' in: '{LocalInstallationRootDirectory}'.", executableName, localInstallationRootDirectory); + + try + { + // + // First, check the root directory itself: + // + var rootExecutablePath = Path.Combine(localInstallationRootDirectory, executableName); + if (File.Exists(rootExecutablePath)) + { + if (shouldLog) + LOGGER.LogInformation("Found local Pandoc installation at the root path: '{Path}'.", rootExecutablePath); + + HAS_LOGGED_ONCE = true; + return new(rootExecutablePath, true); + } + + // + // Then, search all subdirectories: + // + var subdirectories = Directory.GetDirectories(localInstallationRootDirectory, "*", SearchOption.AllDirectories); + foreach (var subdirectory in subdirectories) + { + var pandocPath = Path.Combine(subdirectory, executableName); + if (File.Exists(pandocPath)) + { + if (shouldLog) + LOGGER.LogInformation("Found local Pandoc installation at: '{Path}'.", pandocPath); + + HAS_LOGGED_ONCE = true; + return new(pandocPath, true); + } + } + + if (shouldLog) + LOGGER.LogWarning("No Pandoc executable found in local installation directory or its subdirectories."); + } + catch (Exception ex) + { + if (shouldLog) + LOGGER.LogWarning(ex, "Error while searching for a local Pandoc installation in: '{LocalInstallationRootDirectory}'.", localInstallationRootDirectory); + } + } + + // + // When no local installation was found, we assume that the pandoc executable is in the system PATH: + // + if (shouldLog) + LOGGER.LogWarning("Falling back to system PATH for the Pandoc executable: '{ExecutableName}'.", PandocExecutableName); + + return new(PandocExecutableName, false); } - catch + finally { - // ignored + HAS_LOGGED_ONCE = true; } - - // - // When no local installation was found, we assume that the pandoc executable is in the system PATH. - // - return new(PandocExecutableName, false); } /// diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.1.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.1.2.md index 00cb3d1d..2fa37abb 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.1.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.1.2.md @@ -1,11 +1,12 @@ # v26.1.2, build 232 (2026-01-xx xx:xx UTC) - Added the option to hide specific assistants by configuration plugins. This is useful for enterprise environments in organizations. - Added the current date and time to the system prompt for better context in conversations. Thanks Peer `peerschuett` for the contribution. -- Added error handling for context window overflow, which can occur with huge file attachments in chats or the document analysis assistant. +- Added error handling for the context window overflow, which can occur with huge file attachments in chats or the document analysis assistant. - Improved error handling for model loading in provider dialogs (LLMs, embeddings, transcriptions). - Improved the microphone handling (transcription preview) so that all sound effects and the voice recording are processed without interruption. - Improved the handling of self-hosted providers in the configuration dialogs (LLMs, embeddings, and transcriptions) when the host cannot provide a list of models. - Improved the document analysis assistant (in preview) by allowing users to send results to a new chat to ask follow-up questions. Thanks to Sabrina `Sabrina-devops` for this contribution. +- Improved the developer experience by detecting incorrect CPU architecture metadata when checking and installing the Pandoc dependency. - Improved error messages for failed communication with AI servers. - Fixed a logging bug that prevented log events from being recorded in some cases. - Fixed a bug that allowed adding a provider (LLM, embedding, or transcription) without selecting a model. diff --git a/app/SharedTools/RIDExtensions.cs b/app/SharedTools/RIDExtensions.cs index 015357f9..d340e9a2 100644 --- a/app/SharedTools/RIDExtensions.cs +++ b/app/SharedTools/RIDExtensions.cs @@ -1,7 +1,39 @@ +using System.Runtime.InteropServices; + namespace SharedTools; public static class RIDExtensions { + /// + /// Detects the current Runtime Identifier (RID) at runtime based on OS and architecture. + /// + /// + /// This method should be preferred over reading the RID from metadata, + /// as the metadata may contain stale values in development environments. + /// + /// The detected RID for the current platform. + public static RID GetCurrentRID() + { + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + var arch = RuntimeInformation.OSArchitecture; + + return (isWindows, isLinux, isMacOS, arch) switch + { + (true, _, _, Architecture.X64) => RID.WIN_X64, + (true, _, _, Architecture.Arm64) => RID.WIN_ARM64, + + (_, true, _, Architecture.X64) => RID.LINUX_X64, + (_, true, _, Architecture.Arm64) => RID.LINUX_ARM64, + + (_, _, true, Architecture.X64) => RID.OSX_X64, + (_, _, true, Architecture.Arm64) => RID.OSX_ARM64, + + _ => RID.NONE, + }; + } + public static string AsMicrosoftRid(this RID rid) => rid switch { RID.WIN_X64 => "win-x64", diff --git a/runtime/src/pandoc.rs b/runtime/src/pandoc.rs index 47ca1626..f2dc6a8f 100644 --- a/runtime/src/pandoc.rs +++ b/runtime/src/pandoc.rs @@ -1,9 +1,14 @@ use std::path::{Path, PathBuf}; use std::fs; +use std::sync::OnceLock; +use log::warn; use tokio::process::Command; use crate::environment::DATA_DIRECTORY; use crate::metadata::META_DATA; +/// Tracks whether the RID mismatch warning has been logged. +static HAS_LOGGED_RID_MISMATCH: OnceLock<()> = OnceLock::new(); + pub struct PandocExecutable { pub executable: String, pub is_local_installation: bool, @@ -156,13 +161,51 @@ impl PandocProcessBuilder { Err("Executable not found".into()) } - /// Reads the os platform to determine the used executable name. + /// Determines the executable name based on the current OS at runtime. + /// + /// This uses runtime detection instead of metadata to ensure correct behavior + /// on dev machines where the metadata may contain stale values. fn pandoc_executable_name() -> String { - let metadata = META_DATA.lock().unwrap(); - let metadata = metadata.as_ref().unwrap(); + // Log a warning (once) if the runtime OS differs from the metadata architecture. + // This can happen on dev machines where the metadata.txt contains stale values. + HAS_LOGGED_RID_MISMATCH.get_or_init(|| { + let runtime_os = std::env::consts::OS; + let runtime_arch = std::env::consts::ARCH; - match metadata.architecture.as_str() { - "win-arm64" | "win-x64" => "pandoc.exe".to_string(), + if let Ok(metadata) = META_DATA.lock() { + if let Some(metadata) = metadata.as_ref() { + let metadata_arch = &metadata.architecture; + + // Determine expected OS from metadata: + let metadata_is_windows = metadata_arch.starts_with("win-"); + let metadata_is_macos = metadata_arch.starts_with("osx-"); + let metadata_is_linux = metadata_arch.starts_with("linux-"); + + // Compare with runtime OS: + let runtime_is_windows = runtime_os == "windows"; + let runtime_is_macos = runtime_os == "macos"; + let runtime_is_linux = runtime_os == "linux"; + + let os_mismatch = (metadata_is_windows != runtime_is_windows) + || (metadata_is_macos != runtime_is_macos) + || (metadata_is_linux != runtime_is_linux); + + if os_mismatch { + warn!( + Source = "Pandoc"; + "Runtime-detected OS '{}-{}' differs from metadata architecture '{}'. Using runtime-detected OS. This is expected on dev machines where metadata.txt may be outdated.", + runtime_os, + runtime_arch, + metadata_arch + ); + } + } + } + }); + + // Use std::env::consts::OS for runtime detection instead of metadata + match std::env::consts::OS { + "windows" => "pandoc.exe".to_string(), _ => "pandoc".to_string(), } }