diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml
index 8d1d8de4..091faafb 100644
--- a/.github/workflows/build-and-release.yml
+++ b/.github/workflows/build-and-release.yml
@@ -173,6 +173,9 @@ jobs:
pdfium_version=$(sed -n '11p' metadata.txt)
pdfium_version=$(echo $pdfium_version | cut -d'.' -f3)
+ # Next line is the Qdrant version:
+ qdrant_version="v$(sed -n '12p' metadata.txt)"
+
# Write the metadata to the environment:
echo "APP_VERSION=${app_version}" >> $GITHUB_ENV
echo "FORMATTED_APP_VERSION=${formatted_app_version}" >> $GITHUB_ENV
@@ -185,6 +188,7 @@ jobs:
echo "TAURI_VERSION=${tauri_version}" >> $GITHUB_ENV
echo "ARCHITECTURE=${{ matrix.dotnet_runtime }}" >> $GITHUB_ENV
echo "PDFIUM_VERSION=${pdfium_version}" >> $GITHUB_ENV
+ echo "QDRANT_VERSION=${qdrant_version}" >> $GITHUB_ENV
# Log the metadata:
echo "App version: '${formatted_app_version}'"
@@ -197,6 +201,7 @@ jobs:
echo "Tauri version: '${tauri_version}'"
echo "Architecture: '${{ matrix.dotnet_runtime }}'"
echo "PDFium version: '${pdfium_version}'"
+ echo "Qdrant version: '${qdrant_version}'"
- name: Read and format metadata (Windows)
if: matrix.platform == 'windows-latest'
@@ -241,6 +246,9 @@ jobs:
$pdfium_version = $metadata[10]
$pdfium_version = $pdfium_version.Split('.')[2]
+ # Next line is the necessary Qdrant version:
+ $qdrant_version = "v$($metadata[11])"
+
# 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
@@ -334,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 }}
@@ -385,6 +395,128 @@ 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 }}
+ RUST_TARGET: ${{ matrix.rust_target }}
+ run: |
+ set -e
+
+ # Target directory:
+ TDB_DIR="runtime/target/databases/qdrant"
+ mkdir -p "$TDB_DIR"
+
+ case "${DOTNET_RUNTIME}" in
+ linux-x64)
+ QDRANT_FILE="x86_64-unknown-linux-gnu.tar.gz"
+ DB_SOURCE="qdrant"
+ DB_TARGET="qdrant-${RUST_TARGET}"
+ ;;
+ linux-arm64)
+ QDRANT_FILE="aarch64-unknown-linux-musl.tar.gz"
+ DB_SOURCE="qdrant"
+ DB_TARGET="qdrant-${RUST_TARGET}"
+ ;;
+ osx-x64)
+ QDRANT_FILE="x86_64-apple-darwin.tar.gz"
+ DB_SOURCE="qdrant"
+ DB_TARGET="qdrant-${RUST_TARGET}"
+ ;;
+ osx-arm64)
+ QDRANT_FILE="aarch64-apple-darwin.tar.gz"
+ DB_SOURCE="qdrant"
+ DB_TARGET="qdrant-${RUST_TARGET}"
+ ;;
+ *)
+ echo "Unknown platform: ${DOTNET_RUNTIME}"
+ exit 1
+ ;;
+ esac
+
+ QDRANT_URL="https://github.com/qdrant/qdrant/releases/download/${QDRANT_VERSION}/qdrant-${QDRANT_FILE}"
+
+ echo "Download Qdrant $QDRANT_URL ..."
+ TMP=$(mktemp -d)
+ ARCHIVE="${TMP}/qdrant.tgz"
+
+ curl -fsSL -o "$ARCHIVE" "$QDRANT_URL"
+
+ echo "Extracting Qdrant ..."
+ tar xzf "$ARCHIVE" -C "$TMP"
+ SRC="${TMP}/${DB_SOURCE}"
+
+ if [ ! -f "$SRC" ]; then
+ echo "Was not able to find Qdrant source: $SRC"
+ exit 1
+ fi
+
+ echo "Copy Qdrant from ${DB_TARGET} to ${TDB_DIR}/"
+ cp -f "$SRC" "$TDB_DIR/$DB_TARGET"
+
+ echo "Cleaning up ..."
+ rm -fr "$TMP"
+
+ - name: Deploy Qdrant (Windows)
+ if: matrix.platform == 'windows-latest'
+ env:
+ QDRANT_VERSION: ${{ env.QDRANT_VERSION }}
+ DOTNET_RUNTIME: ${{ matrix.dotnet_runtime }}
+ RUST_TARGET: ${{ matrix.rust_target }}
+ run: |
+ $TDB_DIR = "runtime\target\databases\qdrant"
+ New-Item -ItemType Directory -Force -Path $TDB_DIR | Out-Null
+
+ switch ($env:DOTNET_RUNTIME) {
+ "win-x64" {
+ $QDRANT_FILE = "x86_64-pc-windows-msvc.zip"
+ $DB_SOURCE = "qdrant.exe"
+ $DB_TARGET = "qdrant-$($env:RUST_TARGET).exe"
+ }
+ "win-arm64" {
+ $QDRANT_FILE = "x86_64-pc-windows-msvc.zip"
+ $DB_SOURCE = "qdrant.exe"
+ $DB_TARGET = "qdrant-$($env:RUST_TARGET).exe"
+ }
+ default {
+ Write-Error "Unknown platform: $($env:DOTNET_RUNTIME)"
+ exit 1
+ }
+ }
+
+ $QDRANT_URL = "https://github.com/qdrant/qdrant/releases/download/$($env:QDRANT_VERSION)/qdrant-$QDRANT_FILE"
+ Write-Host "Download $QDRANT_URL ..."
+
+ # Create a unique temporary directory (not just a file)
+ $TMP = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
+ New-Item -ItemType Directory -Path $TMP -Force | Out-Null
+ $ARCHIVE = Join-Path $TMP "qdrant.tgz"
+
+ Invoke-WebRequest -Uri $QDRANT_URL -OutFile $ARCHIVE
+
+ Write-Host "Extracting Qdrant ..."
+ tar -xzf $ARCHIVE -C $TMP
+
+ $SRC = Join-Path $TMP $DB_SOURCE
+ if (!(Test-Path $SRC)) {
+ Write-Error "Cannot find Qdrant source: $SRC"
+ exit 1
+ }
+
+ $DEST = Join-Path $TDB_DIR $DB_TARGET
+ Copy-Item -Path $SRC -Destination $DEST -Force
+
+ Write-Host "Cleaning up ..."
+ Remove-Item $ARCHIVE -Force -ErrorAction SilentlyContinue
+
# Try to remove the temporary directory, but ignore errors if files are still in use
try {
Remove-Item $TMP -Recurse -Force -ErrorAction Stop
@@ -821,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/.gitignore b/.gitignore
index 81a01256..3175fdb1 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
@@ -159,3 +166,6 @@ orleans.codegen.cs
# Ignore AI plugin config files:
/app/.idea/.idea.MindWork AI Studio/.idea/AugmentWebviewStateStore.xml
+
+# Ignore GitHub Copilot migration files:
+**/copilot.data.migration.*.xml
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 00000000..58e2b866
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,15 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/contentModel.xml
+/modules.xml
+/projectSettingsUpdater.xml
+/.idea.mindwork-ai-studio.iml
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 00000000..df87cf95
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/indexLayout.xml b/.idea/indexLayout.xml
new file mode 100644
index 00000000..7b08163c
--- /dev/null
+++ b/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..02078f06
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,185 @@
+# AGENTS.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+MindWork AI Studio is a cross-platform desktop application for interacting with Large Language Models (LLMs). The app uses a hybrid architecture combining a Rust Tauri runtime (for the native desktop shell) with a .NET Blazor Server web application (for the UI and business logic).
+
+**Key Architecture Points:**
+- **Runtime:** Rust-based Tauri v1.8 application providing the native window, system integration, and IPC layer
+- **App:** .NET 9 Blazor Server application providing the UI and core functionality
+- **Communication:** The Rust runtime and .NET app communicate via HTTPS with TLS certificates generated at startup
+- **Providers:** Multi-provider architecture supporting OpenAI, Anthropic, Google, Mistral, Perplexity, self-hosted models, and others
+- **Plugin System:** Lua-based plugin system for language packs, configuration, and future assistant plugins
+
+## Building
+
+### Prerequisites
+- .NET 9 SDK
+- Rust toolchain (stable)
+- Tauri v1.6.2 CLI: `cargo install --version 1.6.2 tauri-cli`
+- Tauri prerequisites (platform-specific dependencies)
+- **Note:** Development on Linux is discouraged due to complex Tauri dependencies that vary by distribution
+
+### Build
+```bash
+cd app/Build
+dotnet run build
+```
+This builds the .NET app as a Tauri "sidecar" binary, which is required even for development.
+
+
+### Running Tests
+Currently, no automated test suite exists in the repository.
+
+## Architecture Details
+
+### Rust Runtime (`runtime/`)
+**Entry point:** `runtime/src/main.rs`
+
+Key modules:
+- `app_window.rs` - Tauri window management, updater integration
+- `dotnet.rs` - Launches and manages the .NET sidecar process
+- `runtime_api.rs` - Rocket-based HTTPS API for .NET ↔ Rust communication
+- `certificate.rs` - Generates self-signed TLS certificates for secure IPC
+- `secret.rs` - Secure secret storage using OS keyring (Keychain/Credential Manager)
+- `clipboard.rs` - Cross-platform clipboard operations
+- `file_data.rs` - File processing for RAG (extracts text from PDF, DOCX, XLSX, PPTX, etc.)
+- `encryption.rs` - AES-256-CBC encryption for sensitive data
+- `pandoc.rs` - Integration with Pandoc for document conversion
+- `log.rs` - Logging infrastructure using `flexi_logger`
+
+### .NET App (`app/MindWork AI Studio/`)
+**Entry point:** `app/MindWork AI Studio/Program.cs`
+
+Key structure:
+- **Program.cs** - Bootstraps Blazor Server, configures Kestrel, initializes encryption and Rust service
+- **Provider/** - LLM provider implementations (OpenAI, Anthropic, Google, Mistral, etc.)
+ - `BaseProvider.cs` - Abstract base for all providers with streaming support
+ - `IProvider.cs` - Provider interface defining capabilities and streaming methods
+- **Chat/** - Chat functionality and message handling
+- **Assistants/** - Pre-configured assistants (translation, summarization, coding, etc.)
+ - `AssistantBase.razor` - Base component for all assistants
+- **Agents/** - contains all agents, e.g., for data source selection, context validation, etc.
+ - `AgentDataSourceSelection.cs` - Selects appropriate data sources for queries
+ - `AgentRetrievalContextValidation.cs` - Validates retrieved context relevance
+- **Tools/PluginSystem/** - Lua-based plugin system
+- **Tools/Services/** - Core background services (settings, message bus, data sources, updates)
+- **Tools/Rust/** - .NET wrapper for Rust API calls
+- **Settings/** - Application settings and data models
+- **Components/** - Reusable Blazor components
+- **Pages/** - Top-level page components
+
+### IPC Communication Flow
+1. Rust runtime starts and generates TLS certificate
+2. Rust starts internal HTTPS API on random port
+3. Rust launches .NET sidecar, passing: API port, certificate fingerprint, API token, secret key
+4. .NET reads environment variables and establishes secure HTTPS connection to Rust
+5. .NET requests an app port from Rust, starts Blazor Server on that port
+6. Rust opens Tauri webview pointing to localhost:app_port
+7. Bi-directional communication: .NET ↔ Rust via HTTPS API
+
+### Configuration and Metadata
+- `metadata.txt` - Build metadata (version, build time, component versions) read by both Rust and .NET
+- `startup.env` - Development environment variables (generated by build script)
+- `.NET project` reads metadata.txt at build time and injects as assembly attributes
+
+## Plugin System
+
+**Location:** `app/MindWork AI Studio/Plugins/`
+
+Plugins are written in Lua and provide:
+- **Language plugins** - I18N translations (e.g., German language pack)
+- **Configuration plugins** - Enterprise IT configurations for centrally managed providers, settings
+- **Future:** Assistant plugins for custom assistants
+
+**Example configuration plugin:** `app/MindWork AI Studio/Plugins/configuration/plugin.lua`
+
+Plugins can configure:
+- Self-hosted LLM providers
+- Update behavior
+- Preview features visibility
+- Preselected profiles
+- Chat templates
+- etc.
+
+When adding configuration options, update:
+- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs`: In method `TryProcessConfiguration` register new options.
+- `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs`: In method `LoadAll` check for leftover configuration.
+- The corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)`, when adding config options (in contrast to complex config. objects)
+- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs` for parsing logic of complex configuration objects.
+- `app/MindWork AI Studio/Plugins/configuration/plugin.lua` to document the new configuration option.
+
+## RAG (Retrieval-Augmented Generation)
+
+RAG integration is currently in development (preview feature). Architecture:
+- **External Retrieval Interface (ERI)** - Contract for integrating external data sources
+- **Data Sources** - Local files and external data via ERI servers
+- **Agents** - AI agents select data sources and validate retrieval quality
+- **Embedding providers** - Support for various embedding models
+- **Vector database** - Planned integration with Qdrant for vector storage
+- **File processing** - Extracts text from PDF, DOCX, XLSX via Rust runtime
+
+## Enterprise IT Support
+
+AI Studio supports centralized configuration for enterprise environments:
+- **Registry (Windows)** or **environment variables** (all platforms) specify configuration server URL and ID
+- Configuration downloaded as ZIP containing Lua plugin
+- Checks for updates every ~16 minutes via ETag
+- Allows IT departments to pre-configure providers, settings, and chat templates
+
+**Documentation:** `documentation/Enterprise IT.md`
+
+## Provider Confidence System
+
+Multi-level confidence scheme allows users to control which providers see which data:
+- Confidence levels: e.g. `NONE`, `LOW`, `MEDIUM`, `HIGH`, and some more granular levels
+- Each assistant/feature can require a minimum confidence level
+- Users assign confidence levels to providers based on trust
+
+**Implementation:** `app/MindWork AI Studio/Provider/Confidence.cs`
+
+## Dependencies and Frameworks
+
+**Rust:**
+- Tauri 1.8 - Desktop application framework
+- Rocket 0.5 - HTTPS API server
+- tokio - Async runtime
+- keyring - OS keyring integration
+- pdfium-render - PDF text extraction
+- calamine - Excel file parsing
+
+**.NET:**
+- Blazor Server - UI framework
+- MudBlazor 8.12 - Component library
+- LuaCSharp - Lua scripting engine
+- HtmlAgilityPack - HTML parsing
+- ReverseMarkdown - HTML to Markdown conversion
+
+## Security
+
+- **Encryption:** AES-256-CBC with PBKDF2 key derivation for sensitive data
+- **IPC:** TLS-secured communication with random ports and API tokens
+- **Secrets:** OS keyring for persistent secret storage (API keys, etc.)
+- **Sandboxing:** Tauri provides OS-level sandboxing
+
+## Release Process
+
+1. Create changelog file: `app/MindWork AI Studio/wwwroot/changelog/vX.Y.Z.md`
+2. Commit changelog
+3. Run from `app/Build`: `dotnet run release --action `
+4. Create PR with version bump and changes
+5. After PR merge, maintainer creates git tag: `vX.Y.Z`
+6. GitHub Actions builds release binaries for all platforms
+7. Binaries uploaded to GitHub Releases
+
+## Important Development Notes
+
+- **File changes require Write/Edit tools** - Never use bash commands like `cat <`
+- **Spaces in paths** - Always quote paths with spaces in bash commands
+- **Debug environment** - Reads `startup.env` file with IPC credentials
+- **Production environment** - Runtime launches .NET sidecar with environment variables
+- **MudBlazor** - Component library requires DI setup in Program.cs
+- **Encryption** - Initialized before Rust service is marked ready
+- **Message Bus** - Singleton event bus for cross-component communication inside the .NET app
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..eef4bd20
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1 @@
+@AGENTS.md
\ No newline at end of file
diff --git a/LICENSE.md b/LICENSE.md
index 1a963dc7..ce1a351d 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -6,7 +6,7 @@ FSL-1.1-MIT
## Notice
-Copyright 2025 Thorsten Sommer
+Copyright 2026 Thorsten Sommer
## Terms and Conditions
diff --git a/README.md b/README.md
index d526d2b3..624cbfc8 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)
+- [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))~~
@@ -79,6 +79,8 @@ Since March 2025: We have started developing the plugin system. There will be la
+- v26.1.1: Added the option to attach files, including images, to chat templates; added support for source code file attachments in chats and document analysis; added a preview feature for recording your own voice for transcription; fixed various bugs in provider dialogs and profile selection.
+- v0.10.0: Added support for newer models like Mistral 3 & GPT 5.2, OpenRouter as LLM and embedding provider, the possibility to use file attachments in chats, and support for images as input.
- v0.9.51: Added support for [Perplexity](https://www.perplexity.ai/); citations added so that LLMs can provide source references (e.g., some OpenAI models, Perplexity); added support for OpenAI's Responses API so that all text LLMs from OpenAI now work in MindWork AI Studio, including Deep Research models; web searches are now possible (some OpenAI models, Perplexity).
- v0.9.50: Added support for self-hosted LLMs using [vLLM](https://blog.vllm.ai/2023/06/20/vllm.html).
- v0.9.46: Released our plugin system, a German language plugin, early support for enterprise environments, and configuration plugins. Additionally, we added the Pandoc integration for future data processing and file generation.
@@ -89,8 +91,6 @@ Since March 2025: We have started developing the plugin system. There will be la
- v0.9.31: Added Helmholtz & GWDG as LLM providers. This is a huge improvement for many researchers out there who can use these providers for free. We added DeepSeek as a provider as well.
- v0.9.29: Added agents to support the RAG process (selecting the best data sources & validating retrieved data as part of the augmentation process)
- v0.9.26+: Added RAG for external data sources using our [ERI interface](https://mindworkai.org/#eri---external-retrieval-interface) as a preview feature.
-- v0.9.25: Added [xAI](https://x.ai/) as a new provider. xAI provides their Grok models for generating content.
-- v0.9.23: Added support for OpenAI `o` models (`o1`, `o1-mini`, `o3`, etc.); added also an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant as a preview feature behind the RAG feature flag. Your own ERI server can be used to gain access to, e.g., your enterprise data from within AI Studio.
@@ -105,6 +105,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
**Key advantages:**
- **Free of charge**: The app is free to use, both for personal and commercial purposes.
+- **Democratization of AI**: We want to contribute to the democratization of AI. MindWork AI Studio runs even on low-cost hardware, including computers around 100 € such as Raspberry Pi. This makes the app and its full feature set accessible to people and families with limited budgets. You can start with local LLMs or use affordable cloud models.
- **Independence**: You are not tied to any single provider. Instead, you can choose the providers that best suit your needs. Right now, we support:
- [OpenAI](https://openai.com/) (GPT5, GPT4.1, o1, o3, o4, etc.)
- [Perplexity](https://www.perplexity.ai/)
@@ -114,6 +115,7 @@ MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provi
- [xAI](https://x.ai/) (Grok)
- [DeepSeek](https://www.deepseek.com/en)
- [Alibaba Cloud](https://www.alibabacloud.com) (Qwen)
+ - [OpenRouter](https://openrouter.ai/)
- [Hugging Face](https://huggingface.co/) using their [inference providers](https://huggingface.co/docs/inference-providers/index) such as Cerebras, Nebius, Sambanova, Novita, Hyperbolic, Together AI, Fireworks, Hugging Face
- Self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), and [vLLM](https://github.com/vllm-project/vllm)
- [Groq](https://groq.com/)
diff --git a/app/Build/Commands/CollectI18NKeysCommand.cs b/app/Build/Commands/CollectI18NKeysCommand.cs
index ec7c291f..d36e650a 100644
--- a/app/Build/Commands/CollectI18NKeysCommand.cs
+++ b/app/Build/Commands/CollectI18NKeysCommand.cs
@@ -69,7 +69,10 @@ public sealed partial class CollectI18NKeysCommand
var ns = this.DetermineNamespace(filePath);
var fileInfo = new FileInfo(filePath);
- var name = fileInfo.Name.Replace(fileInfo.Extension, string.Empty).Replace(".razor", string.Empty);
+
+ var name = this.DetermineTypeName(filePath)
+ ?? fileInfo.Name.Replace(fileInfo.Extension, string.Empty).Replace(".razor", string.Empty);
+
var langNamespace = $"{ns}.{name}".ToUpperInvariant();
foreach (var match in matches)
{
@@ -236,6 +239,14 @@ public sealed partial class CollectI18NKeysCommand
Console.WriteLine($"- Error: The file '{filePath}' is neither a C# nor a Razor file. We can't determine the namespace.");
return null;
}
+
+ private string? DetermineTypeName(string filePath)
+ {
+ if (!filePath.EndsWith(".cs", StringComparison.OrdinalIgnoreCase))
+ return null;
+
+ return this.ReadPartialTypeNameFromCSharp(filePath);
+ }
private string? ReadNamespaceFromCSharp(string filePath)
{
@@ -254,6 +265,24 @@ public sealed partial class CollectI18NKeysCommand
var match = matches[0];
return match.Groups[1].Value;
}
+
+ private string? ReadPartialTypeNameFromCSharp(string filePath)
+ {
+ var content = File.ReadAllText(filePath, Encoding.UTF8);
+ var matches = CSharpPartialTypeRegex().Matches(content);
+
+ if (matches.Count == 0)
+ return null;
+
+ if (matches.Count > 1)
+ {
+ Console.WriteLine($"The file '{filePath}' contains multiple partial type declarations. This scenario is not supported.");
+ return null;
+ }
+
+ var match = matches[0];
+ return match.Groups[1].Value;
+ }
private string? ReadNamespaceFromRazor(string filePath)
{
@@ -278,4 +307,7 @@ public sealed partial class CollectI18NKeysCommand
[GeneratedRegex("""namespace\s+([a-zA-Z0-9_.]+)""")]
private static partial Regex CSharpNamespaceRegex();
-}
\ No newline at end of file
+
+ [GeneratedRegex("""\bpartial\s+(?:class|struct|interface|record(?:\s+(?:class|struct))?)\s+([A-Za-z_][A-Za-z0-9_]*)""")]
+ private static partial Regex CSharpPartialTypeRegex();
+}
diff --git a/app/Build/Commands/Database.cs b/app/Build/Commands/Database.cs
new file mode 100644
index 00000000..dcd78391
--- /dev/null
+++ b/app/Build/Commands/Database.cs
@@ -0,0 +1,3 @@
+namespace Build.Commands;
+
+public record Database(string Path, string Filename);
\ No newline at end of file
diff --git a/app/Build/Commands/PrepareAction.cs b/app/Build/Commands/PrepareAction.cs
index 2f2ffcb2..5b383492 100644
--- a/app/Build/Commands/PrepareAction.cs
+++ b/app/Build/Commands/PrepareAction.cs
@@ -3,8 +3,10 @@ namespace Build.Commands;
public enum PrepareAction
{
NONE,
-
- PATCH,
- MINOR,
- MAJOR,
+
+ BUILD,
+ MONTH,
+ YEAR,
+
+ SET,
}
\ No newline at end of file
diff --git a/app/Build/Commands/Qdrant.cs b/app/Build/Commands/Qdrant.cs
new file mode 100644
index 00000000..29369ccf
--- /dev/null
+++ b/app/Build/Commands/Qdrant.cs
@@ -0,0 +1,120 @@
+using System.Formats.Tar;
+using System.IO.Compression;
+
+using SharedTools;
+
+namespace Build.Commands;
+
+public static class Qdrant
+{
+ public static async Task InstallAsync(RID rid, string version)
+ {
+ Console.Write($"- Installing Qdrant {version} for {rid.ToUserFriendlyName()} ...");
+
+ var cwd = Environment.GetRustRuntimeDirectory();
+ var qdrantTmpDownloadPath = Path.GetTempFileName();
+ var qdrantTmpExtractPath = Directory.CreateTempSubdirectory();
+ var qdrantUrl = GetQdrantDownloadUrl(rid, version);
+
+ //
+ // Download the file:
+ //
+ Console.Write(" downloading ...");
+ using (var client = new HttpClient())
+ {
+ var response = await client.GetAsync(qdrantUrl);
+ if (!response.IsSuccessStatusCode)
+ {
+ Console.WriteLine($" failed to download Qdrant {version} for {rid.ToUserFriendlyName()} from {qdrantUrl}");
+ return;
+ }
+
+ await using var fileStream = File.Create(qdrantTmpDownloadPath);
+ await response.Content.CopyToAsync(fileStream);
+ }
+
+ //
+ // Extract the downloaded file:
+ //
+ Console.Write(" extracting ...");
+ await using(var zStream = File.Open(qdrantTmpDownloadPath, FileMode.Open, FileAccess.Read, FileShare.Read))
+ {
+ if (rid == RID.WIN_X64)
+ {
+ using var archive = new ZipArchive(zStream, ZipArchiveMode.Read);
+ archive.ExtractToDirectory(qdrantTmpExtractPath.FullName, overwriteFiles: true);
+ }
+ else
+ {
+ await using var uncompressedStream = new GZipStream(zStream, CompressionMode.Decompress);
+ await TarFile.ExtractToDirectoryAsync(uncompressedStream, qdrantTmpExtractPath.FullName, true);
+ }
+ }
+
+ //
+ // Copy the database to the target directory:
+ //
+ Console.Write(" deploying ...");
+ var database = GetDatabasePath(rid);
+ if (string.IsNullOrWhiteSpace(database.Path))
+ {
+ Console.WriteLine($" failed to find the database path for {rid.ToUserFriendlyName()}");
+ return;
+ }
+
+ var qdrantDbSourcePath = Path.Join(qdrantTmpExtractPath.FullName, database.Path);
+ var qdrantDbTargetPath = Path.Join(cwd, "target", "databases", "qdrant",database.Filename);
+ if (!File.Exists(qdrantDbSourcePath))
+ {
+ Console.WriteLine($" failed to find the database file '{qdrantDbSourcePath}'");
+ return;
+ }
+
+ Directory.CreateDirectory(Path.Join(cwd, "target", "databases", "qdrant"));
+ if (File.Exists(qdrantDbTargetPath))
+ File.Delete(qdrantDbTargetPath);
+
+ File.Copy(qdrantDbSourcePath, qdrantDbTargetPath);
+
+ //
+ // Cleanup:
+ //
+ Console.Write(" cleaning up ...");
+ File.Delete(qdrantTmpDownloadPath);
+ Directory.Delete(qdrantTmpExtractPath.FullName, true);
+
+ Console.WriteLine(" done.");
+ }
+
+ private static Database GetDatabasePath(RID rid) => rid switch
+ {
+ RID.OSX_ARM64 => new("qdrant", "qdrant-aarch64-apple-darwin"),
+ RID.OSX_X64 => new("qdrant", "qdrant-x86_64-apple-darwin"),
+
+ RID.LINUX_ARM64 => new("qdrant", "qdrant-aarch64-unknown-linux-gnu"),
+ RID.LINUX_X64 => new("qdrant", "qdrant-x86_64-unknown-linux-gnu"),
+
+ RID.WIN_X64 => new("qdrant.exe", "qdrant-x86_64-pc-windows-msvc.exe"),
+ RID.WIN_ARM64 => new("qdrant.exe", "qdrant-aarch64-pc-windows-msvc.exe"),
+
+ _ => new(string.Empty, string.Empty),
+ };
+
+ private static string GetQdrantDownloadUrl(RID rid, string version)
+ {
+ var baseUrl = $"https://github.com/qdrant/qdrant/releases/download/v{version}/qdrant-";
+ return rid switch
+ {
+ RID.LINUX_ARM64 => $"{baseUrl}aarch64-unknown-linux-musl.tar.gz",
+ RID.LINUX_X64 => $"{baseUrl}x86_64-unknown-linux-gnu.tar.gz",
+
+ RID.OSX_ARM64 => $"{baseUrl}aarch64-apple-darwin.tar.gz",
+ RID.OSX_X64 => $"{baseUrl}x86_64-apple-darwin.tar.gz",
+
+ RID.WIN_X64 => $"{baseUrl}x86_64-pc-windows-msvc.zip",
+ RID.WIN_ARM64 => $"{baseUrl}x86_64-pc-windows-msvc.zip",
+
+ _ => string.Empty,
+ };
+ }
+}
\ No newline at end of file
diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs
index b9b01357..5ec929ab 100644
--- a/app/Build/Commands/UpdateMetadataCommands.cs
+++ b/app/Build/Commands/UpdateMetadataCommands.cs
@@ -13,13 +13,32 @@ namespace Build.Commands;
public sealed partial class UpdateMetadataCommands
{
[Command("release", Description = "Prepare & build the next release")]
- public async Task Release(PrepareAction action)
+ public async Task Release(
+ [Option("action", ['a'], Description = "The release action: patch, minor, or major")] PrepareAction action = PrepareAction.NONE,
+ [Option("version", ['v'], Description = "Set a specific version directly, e.g., 26.1.2")] string? version = null)
{
if(!Environment.IsWorkingDirectoryValid())
return;
-
+
+ // Validate parameters: either action or version must be specified, but not both:
+ if (action == PrepareAction.NONE && string.IsNullOrWhiteSpace(version))
+ {
+ Console.WriteLine("- Error: You must specify either --action (-a) or --version (-v).");
+ return;
+ }
+
+ if (action != PrepareAction.NONE && !string.IsNullOrWhiteSpace(version))
+ {
+ Console.WriteLine("- Error: You cannot specify both --action and --version. Please use only one.");
+ return;
+ }
+
+ // If version is specified, use SET action:
+ if (!string.IsNullOrWhiteSpace(version))
+ action = PrepareAction.SET;
+
// Prepare the metadata for the next release:
- await this.PerformPrepare(action, true);
+ await this.PerformPrepare(action, true, version);
// Build once to allow the Rust compiler to read the changed metadata
// and to update all .NET artifacts:
@@ -53,11 +72,30 @@ public sealed partial class UpdateMetadataCommands
}
[Command("prepare", Description = "Prepare the metadata for the next release")]
- public async Task Prepare(PrepareAction action)
+ public async Task Prepare(
+ [Option("action", ['a'], Description = "The release action: patch, minor, or major")] PrepareAction action = PrepareAction.NONE,
+ [Option("version", ['v'], Description = "Set a specific version directly, e.g., 26.1.2")] string? version = null)
{
if(!Environment.IsWorkingDirectoryValid())
return;
+ // Validate parameters: either action or version must be specified, but not both:
+ if (action == PrepareAction.NONE && string.IsNullOrWhiteSpace(version))
+ {
+ Console.WriteLine("- Error: You must specify either --action (-a) or --version (-v).");
+ return;
+ }
+
+ if (action != PrepareAction.NONE && !string.IsNullOrWhiteSpace(version))
+ {
+ Console.WriteLine("- Error: You cannot specify both --action and --version. Please use only one.");
+ return;
+ }
+
+ // If version is specified, use SET action:
+ if (!string.IsNullOrWhiteSpace(version))
+ action = PrepareAction.SET;
+
Console.WriteLine("==============================");
Console.Write("- Are you trying to prepare a new release? (y/n) ");
var userAnswer = Console.ReadLine();
@@ -66,18 +104,18 @@ public sealed partial class UpdateMetadataCommands
Console.WriteLine("- Please use the 'release' command instead");
return;
}
-
- await this.PerformPrepare(action, false);
+
+ await this.PerformPrepare(action, false, version);
}
- private async Task PerformPrepare(PrepareAction action, bool internalCall)
+ private async Task PerformPrepare(PrepareAction action, bool internalCall, string? version = null)
{
if(internalCall)
Console.WriteLine("==============================");
-
+
Console.WriteLine("- Prepare the metadata for the next release ...");
-
- var appVersion = await this.UpdateAppVersion(action);
+
+ var appVersion = await this.UpdateAppVersion(action, version);
if (!string.IsNullOrWhiteSpace(appVersion.VersionText))
{
var buildNumber = await this.IncreaseBuildNumber();
@@ -90,7 +128,7 @@ public sealed partial class UpdateMetadataCommands
await this.UpdateTauriVersion();
await this.UpdateProjectCommitHash();
await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "..", "..", "LICENSE.md")));
- await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "About.razor.cs")));
+ await this.UpdateLicenceYear(Path.GetFullPath(Path.Combine(Environment.GetAIStudioDirectory(), "Pages", "Information.razor.cs")));
Console.WriteLine();
}
}
@@ -112,6 +150,9 @@ public sealed partial class UpdateMetadataCommands
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()}");
@@ -239,17 +280,6 @@ public sealed partial class UpdateMetadataCommands
var pathChangelogs = Path.Combine(Environment.GetAIStudioDirectory(), "wwwroot", "changelog");
var nextBuildNumber = currentBuildNumber + 1;
- //
- // We assume that most of the time, there will be patch releases:
- //
- var nextMajor = currentAppVersion.Major;
- var nextMinor = currentAppVersion.Minor;
- var nextPatch = currentAppVersion.Patch + 1;
-
- var nextAppVersion = $"{nextMajor}.{nextMinor}.{nextPatch}";
- var nextChangelogFilename = $"v{nextAppVersion}.md";
- var nextChangelogFilePath = Path.Combine(pathChangelogs, nextChangelogFilename);
-
//
// Regarding the next build time: We assume that the next release will take place in one week from now.
// Thus, we check how many days this month has left. In the end, we want to predict the year and month
@@ -259,6 +289,19 @@ public sealed partial class UpdateMetadataCommands
var nextBuildYear = (DateTime.Today + TimeSpan.FromDays(7)).Year;
var nextBuildTimeString = $"{nextBuildYear}-{nextBuildMonth:00}-xx xx:xx UTC";
+ //
+ // We assume that most of the time, there will be patch releases:
+ //
+ // skipping the first 2 digits for major version
+ var nextBuildYearShort = nextBuildYear - 2000;
+ var nextMajor = nextBuildYearShort;
+ var nextMinor = nextBuildMonth;
+ var nextPatch = currentAppVersion.Major != nextBuildYearShort || currentAppVersion.Minor != nextBuildMonth ? 1 : currentAppVersion.Patch + 1;
+
+ var nextAppVersion = $"{nextMajor}.{nextMinor}.{nextPatch}";
+ var nextChangelogFilename = $"v{nextAppVersion}.md";
+ var nextChangelogFilePath = Path.Combine(pathChangelogs, nextChangelogFilename);
+
var changelogHeader = $"""
# v{nextAppVersion}, build {nextBuildNumber} ({nextBuildTimeString})
@@ -324,6 +367,16 @@ public sealed partial class UpdateMetadataCommands
return shortVersion;
}
+ private async Task ReadQdrantVersion()
+ {
+ const int QDRANT_VERSION_INDEX = 11;
+ var pathMetadata = Environment.GetMetadataPath();
+ var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
+ var currentQdrantVersion = lines[QDRANT_VERSION_INDEX].Trim();
+
+ return currentQdrantVersion;
+ }
+
private async Task UpdateArchitecture(RID rid)
{
const int ARCHITECTURE_INDEX = 9;
@@ -336,8 +389,9 @@ public sealed partial class UpdateMetadataCommands
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
Console.WriteLine(" done.");
}
-
- private async Task UpdateProjectCommitHash()
+
+ [Command("update-project-hash", Description = "Update the project commit hash")]
+ public async Task UpdateProjectCommitHash()
{
const int COMMIT_HASH_INDEX = 8;
@@ -354,49 +408,69 @@ public sealed partial class UpdateMetadataCommands
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
}
- private async Task UpdateAppVersion(PrepareAction action)
+ private async Task UpdateAppVersion(PrepareAction action, string? version = null)
{
const int APP_VERSION_INDEX = 0;
-
+
if (action == PrepareAction.NONE)
{
Console.WriteLine("- No action specified. Skipping app version update.");
return new(string.Empty, 0, 0, 0);
}
-
+
var pathMetadata = Environment.GetMetadataPath();
var lines = await File.ReadAllLinesAsync(pathMetadata, Encoding.UTF8);
var currentAppVersionLine = lines[APP_VERSION_INDEX].Trim();
- var currentAppVersion = AppVersionRegex().Match(currentAppVersionLine);
- var currentPatch = int.Parse(currentAppVersion.Groups["patch"].Value);
- var currentMinor = int.Parse(currentAppVersion.Groups["minor"].Value);
- var currentMajor = int.Parse(currentAppVersion.Groups["major"].Value);
-
- switch (action)
+
+ int newMajor, newMinor, newPatch;
+ if (action == PrepareAction.SET && !string.IsNullOrWhiteSpace(version))
{
- case PrepareAction.PATCH:
- currentPatch++;
- break;
-
- case PrepareAction.MINOR:
- currentPatch = 0;
- currentMinor++;
- break;
-
- case PrepareAction.MAJOR:
- currentPatch = 0;
- currentMinor = 0;
- currentMajor++;
- break;
+ // Parse the provided version string:
+ var versionMatch = AppVersionRegex().Match(version);
+ if (!versionMatch.Success)
+ {
+ Console.WriteLine($"- Error: Invalid version format '{version}'. Expected format: major.minor.patch (e.g., 26.1.2)");
+ return new(string.Empty, 0, 0, 0);
+ }
+
+ newMajor = int.Parse(versionMatch.Groups["major"].Value);
+ newMinor = int.Parse(versionMatch.Groups["minor"].Value);
+ newPatch = int.Parse(versionMatch.Groups["patch"].Value);
}
-
- var updatedAppVersion = $"{currentMajor}.{currentMinor}.{currentPatch}";
+ else
+ {
+ // Parse current version and increment based on action:
+ var currentAppVersion = AppVersionRegex().Match(currentAppVersionLine);
+ newPatch = int.Parse(currentAppVersion.Groups["patch"].Value);
+ newMinor = int.Parse(currentAppVersion.Groups["minor"].Value);
+ newMajor = int.Parse(currentAppVersion.Groups["major"].Value);
+
+ switch (action)
+ {
+ case PrepareAction.BUILD:
+ newPatch++;
+ break;
+
+ case PrepareAction.MONTH:
+ newPatch = 1;
+ newMinor++;
+ break;
+
+ case PrepareAction.YEAR:
+ newPatch = 1;
+ newMinor = 1;
+ newMajor++;
+ break;
+ }
+ }
+
+ var updatedAppVersion = $"{newMajor}.{newMinor}.{newPatch}";
Console.WriteLine($"- Updating app version from '{currentAppVersionLine}' to '{updatedAppVersion}'.");
-
+
lines[APP_VERSION_INDEX] = updatedAppVersion;
await File.WriteAllLinesAsync(pathMetadata, lines, Environment.UTF8_NO_BOM);
-
- return new(updatedAppVersion, currentMajor, currentMinor, currentPatch);
+
+ return new(updatedAppVersion, newMajor, newMinor, newPatch);
}
private async Task UpdateLicenceYear(string licenceFilePath)
diff --git a/app/MindWork AI Studio.sln.DotSettings b/app/MindWork AI Studio.sln.DotSettings
index faaedb6b..51ce5109 100644
--- a/app/MindWork AI Studio.sln.DotSettings
+++ b/app/MindWork AI Studio.sln.DotSettings
@@ -6,6 +6,7 @@
GWDG
HF
IERI
+ IMIME
LLM
LM
MSG
@@ -18,10 +19,14 @@
URL
I18N
True
+ True
True
True
True
True
+ True
True
True
+ True
+ True
True
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs b/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs
index 42d395be..f7947462 100644
--- a/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs
+++ b/app/MindWork AI Studio/Agents/AgentDataSourceSelection.cs
@@ -131,7 +131,7 @@ public sealed class AgentDataSourceSelection (ILogger
#endregion
- public async Task> PerformSelectionAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default)
+ public async Task> PerformSelectionAsync(IProvider provider, IContent lastUserPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default)
{
logger.LogInformation("The AI should select the appropriate data sources.");
@@ -154,12 +154,14 @@ public sealed class AgentDataSourceSelection (ILogger
//
// 2. Prepare the current system and user prompts as input for the agent:
//
- var lastPromptContent = lastPrompt switch
+ var lastPromptContent = lastUserPrompt switch
{
ContentText text => text.Text,
// Image prompts may be empty, e.g., when the image is too large:
- ContentImage image => await image.AsBase64(token),
+ ContentImage image => await image.TryAsBase64(token) is (success: true, { } base64Image)
+ ? base64Image
+ : string.Empty,
// Other content types are not supported yet:
_ => string.Empty,
@@ -188,11 +190,23 @@ public sealed class AgentDataSourceSelection (ILogger
switch (ds)
{
case DataSourceLocalDirectory localDirectory:
- sb.AppendLine($"- Id={ds.Id}, name='{localDirectory.Name}', type=local directory, path='{localDirectory.Path}'");
+ if (string.IsNullOrWhiteSpace(localDirectory.Description))
+ sb.AppendLine($"- Id={ds.Id}, name='{localDirectory.Name}', type=local directory, path='{localDirectory.Path}'");
+ else
+ {
+ var description = localDirectory.Description.Replace("\n", " ").Replace("\r", " ");
+ sb.AppendLine($"- Id={ds.Id}, name='{localDirectory.Name}', type=local directory, path='{localDirectory.Path}', description='{description}'");
+ }
break;
case DataSourceLocalFile localFile:
- sb.AppendLine($"- Id={ds.Id}, name='{localFile.Name}', type=local file, path='{localFile.FilePath}'");
+ if (string.IsNullOrWhiteSpace(localFile.Description))
+ sb.AppendLine($"- Id={ds.Id}, name='{localFile.Name}', type=local file, path='{localFile.FilePath}'");
+ else
+ {
+ var description = localFile.Description.Replace("\n", " ").Replace("\r", " ");
+ sb.AppendLine($"- Id={ds.Id}, name='{localFile.Name}', type=local file, path='{localFile.FilePath}', description='{description}'");
+ }
break;
case IERIDataSource eriDataSource:
diff --git a/app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs b/app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs
index 7d7b9bb6..ee2437d9 100644
--- a/app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs
+++ b/app/MindWork AI Studio/Agents/AgentRetrievalContextValidation.cs
@@ -147,12 +147,12 @@ public sealed class AgentRetrievalContextValidation (ILogger
/// Validate all retrieval contexts against the last user and the system prompt.
///
- /// The last user prompt.
+ /// The last user prompt.
/// The chat thread.
/// All retrieval contexts to validate.
/// The cancellation token.
/// The validation results.
- public async Task> ValidateRetrievalContextsAsync(IContent lastPrompt, ChatThread chatThread, IReadOnlyList retrievalContexts, CancellationToken token = default)
+ public async Task> ValidateRetrievalContextsAsync(IContent lastUserPrompt, ChatThread chatThread, IReadOnlyList retrievalContexts, CancellationToken token = default)
{
// Check if the retrieval context validation is enabled:
if (!this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
@@ -178,7 +178,7 @@ public sealed class AgentRetrievalContextValidation (ILogger
- /// The last user prompt.
+ /// The last user prompt.
/// The chat thread.
/// The retrieval context to validate.
/// The cancellation token.
/// The optional semaphore to limit the number of parallel validations.
/// The validation result.
- public async Task ValidateRetrievalContextAsync(IContent lastPrompt, ChatThread chatThread, IRetrievalContext retrievalContext, CancellationToken token = default, SemaphoreSlim? semaphore = null)
+ public async Task ValidateRetrievalContextAsync(IContent lastUserPrompt, ChatThread chatThread, IRetrievalContext retrievalContext, CancellationToken token = default, SemaphoreSlim? semaphore = null)
{
try
{
@@ -214,12 +214,14 @@ public sealed class AgentRetrievalContextValidation (ILogger text.Text,
// Image prompts may be empty, e.g., when the image is too large:
- ContentImage image => await image.AsBase64(token),
+ ContentImage image => await image.TryAsBase64(token) is (success: true, { } base64Image)
+ ? base64Image
+ : string.Empty,
// Other content types are not supported yet:
_ => string.Empty,
diff --git a/app/MindWork AI Studio/App.razor b/app/MindWork AI Studio/App.razor
index 37492a67..b314b033 100644
--- a/app/MindWork AI Studio/App.razor
+++ b/app/MindWork AI Studio/App.razor
@@ -27,6 +27,7 @@
+