mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-12 11:12:12 +00:00
Merge branch 'main' into chunk-data
This commit is contained in:
commit
00b6c98f3b
173
.github/workflows/build-and-release.yml
vendored
173
.github/workflows/build-and-release.yml
vendored
@ -12,6 +12,10 @@ on:
|
||||
- synchronize
|
||||
- reopened
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && (github.event.action != 'labeled' || github.event.label.name == 'run-pipeline') && github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' && (github.event.action != 'labeled' || github.event.label.name == 'run-pipeline') }}
|
||||
|
||||
env:
|
||||
RETENTION_INTERMEDIATE_ASSETS: 1
|
||||
RETENTION_RELEASE_ASSETS: 30
|
||||
@ -37,6 +41,8 @@ jobs:
|
||||
id: determine
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PR_ACTION: ${{ github.event.action }}
|
||||
ACTION_LABEL_NAME: ${{ github.event.label.name }}
|
||||
REF: ${{ github.ref }}
|
||||
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
|
||||
PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@ -55,6 +61,11 @@ jobs:
|
||||
is_internal_pr=true
|
||||
fi
|
||||
|
||||
has_run_pipeline_label=false
|
||||
if [[ " $PR_LABELS " == *" run-pipeline "* ]]; then
|
||||
has_run_pipeline_label=true
|
||||
fi
|
||||
|
||||
if [[ "$REF" == refs/tags/v* ]]; then
|
||||
is_release=true
|
||||
build_enabled=true
|
||||
@ -65,13 +76,21 @@ jobs:
|
||||
build_enabled=true
|
||||
artifact_retention_days=7
|
||||
skip_reason=""
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && " $PR_LABELS " == *" run-pipeline "* ]]; then
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" == "labeled" && "$ACTION_LABEL_NAME" == "run-pipeline" ]]; then
|
||||
is_labeled_pr=true
|
||||
is_pr_build=true
|
||||
build_enabled=true
|
||||
artifact_retention_days=3
|
||||
skip_reason=""
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && " $PR_LABELS " != *" run-pipeline "* ]]; then
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" != "labeled" && "$has_run_pipeline_label" == "true" ]]; then
|
||||
is_labeled_pr=true
|
||||
is_pr_build=true
|
||||
build_enabled=true
|
||||
artifact_retention_days=3
|
||||
skip_reason=""
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" == "labeled" ]]; then
|
||||
skip_reason="Build disabled: label '${ACTION_LABEL_NAME}' is not 'run-pipeline'."
|
||||
elif [[ "$EVENT_NAME" == "pull_request" && "$has_run_pipeline_label" != "true" ]]; then
|
||||
skip_reason="Build disabled: PR does not have the required 'run-pipeline' label."
|
||||
fi
|
||||
|
||||
@ -220,29 +239,29 @@ jobs:
|
||||
rust_target: 'aarch64-apple-darwin'
|
||||
dotnet_runtime: 'osx-arm64'
|
||||
dotnet_name_postfix: '-aarch64-apple-darwin'
|
||||
tauri_bundle: 'dmg,updater'
|
||||
tauri_bundle: 'dmg,app,updater'
|
||||
tauri_bundle_pr: 'dmg'
|
||||
|
||||
- platform: 'macos-latest' # for Intel-based macOS
|
||||
rust_target: 'x86_64-apple-darwin'
|
||||
dotnet_runtime: 'osx-x64'
|
||||
dotnet_name_postfix: '-x86_64-apple-darwin'
|
||||
tauri_bundle: 'dmg,updater'
|
||||
tauri_bundle: 'dmg,app,updater'
|
||||
tauri_bundle_pr: 'dmg'
|
||||
|
||||
- platform: 'ubuntu-22.04' # for x86-based Linux
|
||||
rust_target: 'x86_64-unknown-linux-gnu'
|
||||
dotnet_runtime: 'linux-x64'
|
||||
dotnet_name_postfix: '-x86_64-unknown-linux-gnu'
|
||||
tauri_bundle: 'appimage,deb,updater'
|
||||
tauri_bundle_pr: 'appimage,deb'
|
||||
tauri_bundle: 'appimage,updater'
|
||||
tauri_bundle_pr: 'appimage'
|
||||
|
||||
- platform: 'ubuntu-22.04-arm' # for ARM-based Linux
|
||||
rust_target: 'aarch64-unknown-linux-gnu'
|
||||
dotnet_runtime: 'linux-arm64'
|
||||
dotnet_name_postfix: '-aarch64-unknown-linux-gnu'
|
||||
tauri_bundle: 'appimage,deb,updater'
|
||||
tauri_bundle_pr: 'appimage,deb'
|
||||
tauri_bundle: 'appimage,updater'
|
||||
tauri_bundle_pr: 'appimage'
|
||||
|
||||
- platform: 'windows-latest' # for x86-based Windows
|
||||
rust_target: 'x86_64-pc-windows-msvc'
|
||||
@ -685,11 +704,9 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin
|
||||
~/.cargo/git/db/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.rustup/toolchains
|
||||
runtime/target
|
||||
|
||||
key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }}
|
||||
@ -699,42 +716,64 @@ jobs:
|
||||
with:
|
||||
toolchain: ${{ env.RUST_VERSION }}
|
||||
targets: ${{ matrix.rust_target }}
|
||||
|
||||
- name: Cache Tauri CLI
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cargo-tauri-cli
|
||||
key: tauri-cli-v2-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Setup dependencies (Ubuntu-specific, x86)
|
||||
if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libfuse2
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils gstreamer1.0-plugins-base gstreamer1.0-plugins-good
|
||||
|
||||
- name: Setup dependencies (Ubuntu-specific, ARM)
|
||||
if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libfuse2
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils gstreamer1.0-plugins-base gstreamer1.0-plugins-good
|
||||
|
||||
- name: Setup Tauri (Unix)
|
||||
if: matrix.platform != 'windows-latest'
|
||||
run: |
|
||||
if ! cargo tauri --version > /dev/null 2>&1; then
|
||||
cargo install --version 1.6.2 tauri-cli
|
||||
echo "$HOME/.cargo-tauri-cli/bin" >> "$GITHUB_PATH"
|
||||
export PATH="$HOME/.cargo-tauri-cli/bin:$PATH"
|
||||
|
||||
if ! cargo tauri --version 2>/dev/null | grep -Eq '^tauri-cli 2\.'; then
|
||||
cargo install tauri-cli --version "^2.11.0" --locked --force --root "$HOME/.cargo-tauri-cli"
|
||||
else
|
||||
echo "Tauri is already installed"
|
||||
echo "Tauri CLI v2 is already installed"
|
||||
fi
|
||||
|
||||
- name: Setup Tauri (Windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: |
|
||||
if (-not (cargo tauri --version 2>$null)) {
|
||||
cargo install --version 1.6.2 tauri-cli
|
||||
"$env:USERPROFILE\.cargo-tauri-cli\bin" >> $env:GITHUB_PATH
|
||||
$env:PATH = "$env:USERPROFILE\.cargo-tauri-cli\bin;$env:PATH"
|
||||
|
||||
$tauriVersion = cargo tauri --version 2>$null
|
||||
if (-not $tauriVersion -or $tauriVersion -notmatch '^tauri-cli 2\.') {
|
||||
cargo install tauri-cli --version "^2.11.0" --locked --force --root "$env:USERPROFILE\.cargo-tauri-cli"
|
||||
} else {
|
||||
Write-Output "Tauri is already installed"
|
||||
Write-Output "Tauri CLI v2 is already installed"
|
||||
}
|
||||
|
||||
- name: Delete previous artifact, which may exist due to caching (macOS)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
run: |
|
||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/dmg/MindWork AI Studio_*.dmg
|
||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/macos/MindWork AI Studio.app.tar.gz*
|
||||
dmg_dir="runtime/target/${{ matrix.rust_target }}/release/bundle/dmg"
|
||||
macos_dir="runtime/target/${{ matrix.rust_target }}/release/bundle/macos"
|
||||
|
||||
if [ -d "$dmg_dir" ]; then
|
||||
find "$dmg_dir" -maxdepth 1 -name 'MindWork AI Studio_*.dmg' -delete
|
||||
fi
|
||||
|
||||
if [ -d "$macos_dir" ]; then
|
||||
find "$macos_dir" -maxdepth 1 -name '*.app' -exec rm -rf {} +
|
||||
find "$macos_dir" -maxdepth 1 -name '*.app.tar.gz*' -delete
|
||||
fi
|
||||
|
||||
- name: Delete previous artifact, which may exist due to caching (Windows - MSI)
|
||||
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi')
|
||||
@ -748,16 +787,11 @@ jobs:
|
||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio_*.exe" -ErrorAction SilentlyContinue
|
||||
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio*nsis.zip*" -ErrorAction SilentlyContinue
|
||||
|
||||
- name: Delete previous artifact, which may exist due to caching (Linux - Debian Package)
|
||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb')
|
||||
run: |
|
||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/deb/mind-work-ai-studio_*.deb
|
||||
|
||||
- name: Delete previous artifact, which may exist due to caching (Linux - AppImage)
|
||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
|
||||
run: |
|
||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio_*.AppImage
|
||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz*
|
||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage
|
||||
rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage.tar.gz*
|
||||
|
||||
- name: Build Tauri project (Unix)
|
||||
if: matrix.platform != 'windows-latest'
|
||||
@ -766,17 +800,39 @@ jobs:
|
||||
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
|
||||
run: |
|
||||
bundles="${{ matrix.tauri_bundle }}"
|
||||
tauri_config_args=()
|
||||
|
||||
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then
|
||||
echo "Running PR test build without updater bundle signing"
|
||||
bundles="${{ matrix.tauri_bundle_pr }}"
|
||||
tauri_config_args=(--config '{"bundle":{"createUpdaterArtifacts":false}}')
|
||||
else
|
||||
export TAURI_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY"
|
||||
export TAURI_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD"
|
||||
export TAURI_SIGNING_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY"
|
||||
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD"
|
||||
fi
|
||||
|
||||
cd runtime
|
||||
cargo tauri build --target ${{ matrix.rust_target }} --bundles "$bundles"
|
||||
cargo tauri build --target ${{ matrix.rust_target }} --bundles "$bundles" "${tauri_config_args[@]}"
|
||||
|
||||
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then
|
||||
updater_artifact_count=$(find target/${{ matrix.rust_target }}/release/bundle -type f \( -name '*.app.tar.gz*' -o -name '*.AppImage.tar.gz*' -o -name '*nsis.zip*' \) | wc -l)
|
||||
|
||||
if [ "$updater_artifact_count" -ne 0 ]; then
|
||||
echo "PR builds must not generate updater artifacts."
|
||||
find target/${{ matrix.rust_target }}/release/bundle -type f \( -name '*.app.tar.gz*' -o -name '*.AppImage.tar.gz*' -o -name '*nsis.zip*' \)
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" != "true" ] && [[ "${{ matrix.platform }}" == macos* ]]; then
|
||||
app_update_archive_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz' | wc -l)
|
||||
app_update_signature_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz.sig' | wc -l)
|
||||
|
||||
if [ "$app_update_archive_count" -eq 0 ] || [ "$app_update_signature_count" -eq 0 ]; then
|
||||
echo "Expected macOS updater artifacts were not generated."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Build Tauri project (Windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
@ -785,17 +841,29 @@ jobs:
|
||||
PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }}
|
||||
run: |
|
||||
$bundles = "${{ matrix.tauri_bundle }}"
|
||||
$tauriConfigArgs = @()
|
||||
|
||||
if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") {
|
||||
Write-Output "Running PR test build without updater bundle signing"
|
||||
$bundles = "${{ matrix.tauri_bundle_pr }}"
|
||||
$tauriConfigArgs = @("--config", '{"bundle":{"createUpdaterArtifacts":false}}')
|
||||
} else {
|
||||
$env:TAURI_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY"
|
||||
$env:TAURI_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD"
|
||||
$env:TAURI_SIGNING_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY"
|
||||
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD"
|
||||
}
|
||||
|
||||
cd runtime
|
||||
cargo tauri build --target ${{ matrix.rust_target }} --bundles $bundles
|
||||
cargo tauri build --target ${{ matrix.rust_target }} --bundles $bundles @tauriConfigArgs
|
||||
|
||||
if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") {
|
||||
$updaterArtifacts = Get-ChildItem -Path "target/${{ matrix.rust_target }}/release/bundle" -Recurse -File -Include "*.app.tar.gz*", "*.AppImage.tar.gz*", "*nsis.zip*" -ErrorAction SilentlyContinue
|
||||
|
||||
if ($updaterArtifacts.Count -ne 0) {
|
||||
Write-Error "PR builds must not generate updater artifacts."
|
||||
$updaterArtifacts | ForEach-Object { Write-Error $_.FullName }
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
- name: Upload artifact (macOS)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
@ -804,7 +872,7 @@ jobs:
|
||||
name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }})
|
||||
path: |
|
||||
runtime/target/${{ matrix.rust_target }}/release/bundle/dmg/MindWork AI Studio_*.dmg
|
||||
runtime/target/${{ matrix.rust_target }}/release/bundle/macos/MindWork AI Studio.app.tar.gz*
|
||||
runtime/target/${{ matrix.rust_target }}/release/bundle/macos/*.app.tar.gz*
|
||||
if-no-files-found: error
|
||||
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
||||
|
||||
@ -830,24 +898,14 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
||||
|
||||
- name: Upload artifact (Linux - Debian Package)
|
||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: MindWork AI Studio (Linux - deb ${{ matrix.dotnet_runtime }})
|
||||
path: |
|
||||
runtime/target/${{ matrix.rust_target }}/release/bundle/deb/mind-work-ai-studio_*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
||||
|
||||
- name: Upload artifact (Linux - AppImage)
|
||||
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }})
|
||||
path: |
|
||||
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio_*.AppImage
|
||||
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz*
|
||||
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage
|
||||
runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage.tar.gz*
|
||||
if-no-files-found: error
|
||||
retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }}
|
||||
|
||||
@ -883,14 +941,14 @@ jobs:
|
||||
# Find and process files in the artifacts directory:
|
||||
find "$GITHUB_WORKSPACE/artifacts" -type f | while read -r FILE; do
|
||||
|
||||
if [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz" ]]; then
|
||||
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz"
|
||||
elif [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
||||
if [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
||||
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz.sig"
|
||||
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz" ]]; then
|
||||
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz"
|
||||
elif [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz" ]]; then
|
||||
TARGET_NAME="MindWork AI Studio_x64.app.tar.gz"
|
||||
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz.sig" ]]; then
|
||||
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz.sig"
|
||||
elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz" ]]; then
|
||||
TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz"
|
||||
else
|
||||
TARGET_NAME="$(basename "$FILE")"
|
||||
TARGET_NAME=$(echo "$TARGET_NAME" | sed "s/_${VERSION}//")
|
||||
@ -941,9 +999,9 @@ jobs:
|
||||
platform="linux-x86_64"
|
||||
elif [[ "$sig_file" == *"aarch64.AppImage"* ]]; then
|
||||
platform="linux-aarch64"
|
||||
elif [[ "$sig_file" == *"x64-setup.nsis"* ]]; then
|
||||
elif [[ "$sig_file" == *"x64-setup"* ]]; then
|
||||
platform="windows-x86_64"
|
||||
elif [[ "$sig_file" == *"arm64-setup.nsis"* ]]; then
|
||||
elif [[ "$sig_file" == *"arm64-setup"* ]]; then
|
||||
platform="windows-aarch64"
|
||||
else
|
||||
echo "Platform not recognized: '$sig_file'"
|
||||
@ -1007,6 +1065,13 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for platform in darwin-aarch64 darwin-x86_64 linux-aarch64 linux-x86_64 windows-aarch64 windows-x86_64; do
|
||||
if ! jq -e --arg platform "$platform" '.platforms[$platform]' $GITHUB_WORKSPACE/release/assets/latest.json > /dev/null; then
|
||||
echo "The generated latest.json is missing platform '$platform'."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Show all release assets
|
||||
run: ls -Rlhat $GITHUB_WORKSPACE/release/assets
|
||||
|
||||
@ -1113,7 +1178,7 @@ jobs:
|
||||
with:
|
||||
prerelease: true
|
||||
draft: false
|
||||
make_latest: true
|
||||
make_latest: false
|
||||
body: ${{ env.CHANGELOG }}
|
||||
name: "Release ${{ env.FORMATTED_VERSION }}"
|
||||
fail_on_unmatched_files: true
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -169,3 +169,6 @@ orleans.codegen.cs
|
||||
|
||||
# Ignore GitHub Copilot migration files:
|
||||
**/copilot.data.migration.*.xml
|
||||
|
||||
# Tauri generated schemas/manifests
|
||||
/runtime/gen/
|
||||
|
||||
@ -49,7 +49,7 @@ Currently, no automated test suite exists in the repository.
|
||||
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
|
||||
- `runtime_api.rs` - Axum-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
|
||||
@ -152,7 +152,7 @@ Multi-level confidence scheme allows users to control which providers see which
|
||||
|
||||
**Rust:**
|
||||
- Tauri 1.8 - Desktop application framework
|
||||
- Rocket - HTTPS API server
|
||||
- Axum - HTTPS API server
|
||||
- tokio - Async runtime
|
||||
- keyring - OS keyring integration
|
||||
- pdfium-render - PDF text extraction
|
||||
@ -187,6 +187,7 @@ Multi-level confidence scheme allows users to control which providers see which
|
||||
- **File changes require Write/Edit tools** - Never use bash commands like `cat <<EOF` or `echo >`
|
||||
- **End of file formatting** - Do not append an extra empty line at the end of files.
|
||||
- **No automated formatting for Rust or .NET files** - Never run automated formatters on Rust files (`.rs`) or .NET files (`.cs`, `.razor`, `.csproj`, etc.). Only make the minimal manual formatting changes required for the specific edit.
|
||||
- **I18N resources are generated** - Do not manually edit `app/MindWork AI Studio/Assistants/I18N/allTexts.lua`, `app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua`, or `app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua`. These files are updated automatically by the I18N process.
|
||||
- **Spaces in paths** - Always quote paths with spaces in bash commands
|
||||
- **Agent-run .NET builds** - Do not run `.NET` builds from an agent. Ask the user to run the build locally in their IDE, preferably via `cd app/Build && dotnet run build` in an IDE terminal, then wait for their feedback before continuing.
|
||||
- **Debug environment** - Reads `startup.env` file with IPC credentials
|
||||
|
||||
@ -28,12 +28,11 @@ Since November 2024: Work on RAG (integration of your data and files) has begun.
|
||||
- [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
|
||||
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
|
||||
- [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~
|
||||
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
|
||||
- [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))~~
|
||||
- [x] ~~App: Implement external embedding providers ([PR #654](https://github.com/MindWorkAI/AI-Studio/pull/654))~~
|
||||
- [ ] App: Implement the process to vectorize one local file using embeddings
|
||||
- [ ] App: Implement the process to vectorize one local file using embeddings (PR [#756](https://github.com/MindWorkAI/AI-Studio/pull/756))
|
||||
- [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
|
||||
- [ ] App: Implement the continuous process of vectorizing data (PR [#756](https://github.com/MindWorkAI/AI-Studio/pull/756))
|
||||
- [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))~~
|
||||
- [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
|
||||
@ -79,6 +78,7 @@ Since March 2025: We have started developing the plugin system. There will be la
|
||||
</h3>
|
||||
</summary>
|
||||
|
||||
- v26.5.5: Released voice recording and transcription for all users; added support for multiple chats running at the same time, export options for profiles, chat templates, and ERI data sources, organization-managed ERI servers, and configurable request timeouts; upgraded the native runtime to Tauri v2.
|
||||
- v26.4.1: Added support for the latest AI models, assistant plugins, a slide planner assistant, a prompt optimization assistant, math rendering in chats, and a configurable start page; released the document analysis assistant and improved enterprise deployment, chat performance, file attachments, and reliability across voice recording, logging, and provider validation.
|
||||
- v26.2.2: Added Qdrant as a building block for our local RAG preview, added an embedding test option to validate embedding providers, and improved enterprise and configuration plugins with preselected providers, additive preview features, support for multiple configurations, and more reliable synchronization.
|
||||
- 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.
|
||||
@ -90,7 +90,6 @@ Since March 2025: We have started developing the plugin system. There will be la
|
||||
- v0.9.44: Added PDF import to the text summarizer, translation, and legal check assistants, allowing you to import PDF files and use them as input for the assistants.
|
||||
- v0.9.40: Added support for the `o4` models from OpenAI. Also, we added Alibaba Cloud & Hugging Face as LLM providers.
|
||||
- v0.9.39: Added the plugin system as a preview feature.
|
||||
- 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.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<attachedFolders>
|
||||
<Path>../../mindwork-ai-studio</Path>
|
||||
</attachedFolders>
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
|
||||
@ -245,7 +245,7 @@ public sealed partial class UpdateMetadataCommands
|
||||
Console.WriteLine("- Start building the Rust runtime ...");
|
||||
|
||||
var pathRuntime = Environment.GetRustRuntimeDirectory();
|
||||
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --bundles none", true);
|
||||
var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --no-bundle", true);
|
||||
var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries);
|
||||
var foundRustIssue = false;
|
||||
foreach (var buildOutputLine in rustBuildOutputLines)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERIV/@EntryIndexedValue">ERIV</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FNV/@EntryIndexedValue">FNV</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HF/@EntryIndexedValue">HF</s:String>
|
||||
|
||||
@ -328,22 +328,40 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
|
||||
this.isProcessing = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
this.ChatThread = await aiText.CreateFromProviderAsync(this.ProviderSettings.CreateProvider(), this.ProviderSettings.Model, this.LastUserPrompt, this.ChatThread, this.CancellationTokenSource!.Token);
|
||||
|
||||
this.isProcessing = false;
|
||||
this.StateHasChanged();
|
||||
|
||||
if(manageCancellationLocally)
|
||||
try
|
||||
{
|
||||
this.CancellationTokenSource.Dispose();
|
||||
this.CancellationTokenSource = null;
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
this.ChatThread = await aiText.CreateFromProviderAsync(this.ProviderSettings.CreateProvider(), this.ProviderSettings.Model, this.LastUserPrompt, this.ChatThread, this.CancellationTokenSource!.Token);
|
||||
|
||||
// Return the AI response:
|
||||
return aiText.Text;
|
||||
}
|
||||
catch (ProviderRequestException e)
|
||||
{
|
||||
this.Logger.LogError(e, "The provider request failed for assistant '{AssistantTitle}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", this.Title, e.StatusCode, e.ReasonPhrase, e.ResponseBody);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
|
||||
|
||||
if (this.resultingContentBlock is not null && string.IsNullOrWhiteSpace(aiText.Text))
|
||||
{
|
||||
this.ChatThread?.Blocks.Remove(this.resultingContentBlock);
|
||||
this.resultingContentBlock = null;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isProcessing = false;
|
||||
this.StateHasChanged();
|
||||
|
||||
// Return the AI response:
|
||||
return aiText.Text;
|
||||
if(manageCancellationLocally)
|
||||
{
|
||||
this.CancellationTokenSource?.Dispose();
|
||||
this.CancellationTokenSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CancelStreaming()
|
||||
|
||||
@ -10,6 +10,8 @@ using AIStudio.Settings.DataModel;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
||||
|
||||
namespace AIStudio.Assistants.DocumentAnalysis;
|
||||
@ -747,16 +749,12 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
|
||||
return $$"""
|
||||
CONFIG["DOCUMENT_ANALYSIS_POLICIES"][#CONFIG["DOCUMENT_ANALYSIS_POLICIES"]+1] = {
|
||||
["Id"] = "{{id}}",
|
||||
["PolicyName"] = "{{this.selectedPolicy.PolicyName.Trim()}}",
|
||||
["PolicyDescription"] = "{{this.selectedPolicy.PolicyDescription.Trim()}}",
|
||||
["PolicyName"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.PolicyName.Trim())}},
|
||||
["PolicyDescription"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.PolicyDescription.Trim())}},
|
||||
|
||||
["AnalysisRules"] = [===[
|
||||
{{this.selectedPolicy.AnalysisRules.Trim()}}
|
||||
]===],
|
||||
["AnalysisRules"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.AnalysisRules.Trim(), forceLongString: true)}},
|
||||
|
||||
["OutputRules"] = [===[
|
||||
{{this.selectedPolicy.OutputRules.Trim()}}
|
||||
]===],
|
||||
["OutputRules"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.OutputRules.Trim(), forceLongString: true)}},
|
||||
|
||||
-- Optional: minimum provider confidence required for this policy.
|
||||
-- Allowed values are: NONE, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH
|
||||
|
||||
@ -2647,6 +2647,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1599198973"]
|
||||
-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."
|
||||
|
||||
-- seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds"
|
||||
|
||||
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled."
|
||||
|
||||
@ -2695,6 +2698,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"]
|
||||
-- Spellchecking is enabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
|
||||
|
||||
-- Request timeout
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3569531009"] = "Request timeout"
|
||||
|
||||
-- App Options
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App Options"
|
||||
|
||||
@ -2722,6 +2728,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"]
|
||||
-- Select a transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
|
||||
|
||||
-- How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] = "How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads."
|
||||
|
||||
-- Navigation bar behavior
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior"
|
||||
|
||||
@ -3124,6 +3133,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Start rec
|
||||
-- Transcription in progress...
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2851219233"] = "Transcription in progress..."
|
||||
|
||||
-- Unfortunately, there was an error communicating with the AI system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T3236134591"] = "Unfortunately, there was an error communicating with the AI system."
|
||||
|
||||
-- The configured transcription provider was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T331613105"] = "The configured transcription provider was not found."
|
||||
|
||||
@ -3637,6 +3649,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2879113658"] =
|
||||
-- Maximum matches per query
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximum matches per query"
|
||||
|
||||
-- Failed to read the user's username from the operating system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2909734556"] = "Failed to read the user's username from the operating system."
|
||||
|
||||
-- Open web link, show more information
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Open web link, show more information"
|
||||
|
||||
@ -3688,6 +3703,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T742006305"] = "
|
||||
-- Embeddings
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Embeddings"
|
||||
|
||||
-- Use the same username and password for all users
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1769874785"] = "Use the same username and password for all users"
|
||||
|
||||
-- Username and password mode
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1787063064"] = "Username and password mode"
|
||||
|
||||
-- How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3081234668"] = "How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?"
|
||||
|
||||
-- User-managed username and password
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T365340972"] = "User-managed username and password"
|
||||
|
||||
-- Export
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3898821075"] = "Export"
|
||||
|
||||
-- Read each user's username from the operating system and share one password
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T76405695"] = "Read each user's username from the operating system and share one password"
|
||||
|
||||
-- Cancel
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T900713019"] = "Cancel"
|
||||
|
||||
-- Describe what data this directory contains to help the AI select it.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Describe what data this directory contains to help the AI select it."
|
||||
|
||||
@ -4753,6 +4789,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T582516016"] =
|
||||
-- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, our templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, our templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them."
|
||||
|
||||
-- Copy attachments into plugin
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1345613295"] = "Copy attachments into plugin"
|
||||
|
||||
-- Delete
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Delete"
|
||||
|
||||
@ -4762,6 +4801,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T15483
|
||||
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts."
|
||||
|
||||
-- Use shared attachment paths
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2054531878"] = "Use shared attachment paths"
|
||||
|
||||
-- No chat templates configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet."
|
||||
|
||||
@ -4780,6 +4822,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T34481
|
||||
-- This template is managed by your organization.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization."
|
||||
|
||||
-- Select configuration plugin folder
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576816894"] = "Select configuration plugin folder"
|
||||
|
||||
-- Edit Chat Template
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template"
|
||||
|
||||
@ -4792,6 +4837,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38650
|
||||
-- Delete Chat Template
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Delete Chat Template"
|
||||
|
||||
-- Export Chat Template
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Export Chat Template"
|
||||
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T975426229"] = "Export configuration"
|
||||
|
||||
-- Which programming language should be preselected for added contexts?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Which programming language should be preselected for added contexts?"
|
||||
|
||||
@ -4854,6 +4905,11 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T146957
|
||||
|
||||
-- Refresh all
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1503082343"] = "Refresh all"
|
||||
-- Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1577531115"] = "Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."
|
||||
|
||||
-- Cannot export this ERI data source because the authentication secret could not be encrypted.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1592527757"] = "Cannot export this ERI data source because the authentication secret could not be encrypted."
|
||||
|
||||
-- External (ERI)
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "External (ERI)"
|
||||
@ -4885,6 +4941,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T269820
|
||||
-- Embedding
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Embedding"
|
||||
|
||||
-- This data source is managed by your organization.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3031462878"] = "This data source is managed by your organization."
|
||||
|
||||
-- Edit
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Edit"
|
||||
|
||||
@ -4911,25 +4970,41 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T354965
|
||||
|
||||
-- Local data sources refresh when files change.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3687976654"] = "Local data sources refresh when files change."
|
||||
-- Export Access Token?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3595669127"] = "Export Access Token?"
|
||||
|
||||
-- Export ERI Data Source
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3831281036"] = "Export ERI Data Source"
|
||||
|
||||
-- Actions
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Actions"
|
||||
|
||||
-- This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T4027572258"] = "This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token."
|
||||
|
||||
-- Configured Data Sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Configured Data Sources"
|
||||
|
||||
-- Add ERI v1 Data Source
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "Add ERI v1 Data Source"
|
||||
|
||||
-- Cannot export this ERI data source because no enterprise encryption secret is configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T750361472"] = "Cannot export this ERI data source because no enterprise encryption secret is configured."
|
||||
|
||||
-- External Data (ERI-Server v1)
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "External Data (ERI-Server v1)"
|
||||
|
||||
-- Local data sources refresh only when triggered manually.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T854231603"] = "Local data sources refresh only when triggered manually."
|
||||
-- Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T782820095"] = "Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"
|
||||
|
||||
-- Local Directory
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Local Directory"
|
||||
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T975426229"] = "Export configuration"
|
||||
|
||||
-- When enabled, you can preselect some ERI server options.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "When enabled, you can preselect some ERI server options."
|
||||
|
||||
@ -5215,6 +5290,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T55364659"
|
||||
-- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile."
|
||||
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T975426229"] = "Export configuration"
|
||||
|
||||
-- Preselect the target language
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Preselect the target language"
|
||||
|
||||
@ -6115,18 +6193,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1890416390"] = "Check for update
|
||||
-- Vision
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision"
|
||||
|
||||
-- In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library."
|
||||
|
||||
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."
|
||||
|
||||
-- Encryption secret: is configured
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
|
||||
|
||||
-- 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"
|
||||
|
||||
@ -6154,6 +6226,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Configuration pl
|
||||
-- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2329884315"] = "The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK."
|
||||
|
||||
-- Linux AppImages bundle GStreamer components to support microphone access and WebM audio recording in the embedded WebKitGTK web view.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T234598990"] = "Linux AppImages bundle GStreamer components to support microphone access and WebM audio recording in the embedded WebKitGTK web view."
|
||||
|
||||
-- Used PDFium version
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2368247719"] = "Used PDFium version"
|
||||
|
||||
@ -6208,6 +6283,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Used .NET runtim
|
||||
-- Explanation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation"
|
||||
|
||||
-- checking availability
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "checking availability"
|
||||
|
||||
-- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software."
|
||||
|
||||
@ -6229,6 +6307,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ide
|
||||
-- Hide Details
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
|
||||
|
||||
-- Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface."
|
||||
|
||||
-- Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3239817808"] = "Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running."
|
||||
|
||||
-- Update Pandoc
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Update Pandoc"
|
||||
|
||||
@ -6253,6 +6337,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs w
|
||||
-- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!"
|
||||
|
||||
-- AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service."
|
||||
|
||||
-- Motivation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
|
||||
|
||||
@ -6262,6 +6349,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available"
|
||||
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
|
||||
|
||||
-- Username provided by the OS
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS"
|
||||
|
||||
-- this version does not met the requirements
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
|
||||
|
||||
@ -6283,6 +6373,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions"
|
||||
-- Database
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database"
|
||||
|
||||
-- This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication."
|
||||
|
||||
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system."
|
||||
|
||||
@ -6303,6 +6396,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK"
|
||||
|
||||
-- We use the DeepSeek Tokenizer to estimate the number of tokens an input will generate.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T591393704"] = "We use the DeepSeek Tokenizer to estimate the number of tokens an input will generate."
|
||||
-- starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "starting"
|
||||
|
||||
-- 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."
|
||||
@ -6325,6 +6420,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Provided by confi
|
||||
-- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate."
|
||||
|
||||
-- Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running."
|
||||
|
||||
-- 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."
|
||||
|
||||
@ -6466,6 +6564,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Your stage directions"
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"
|
||||
|
||||
-- The provider '{0}' reported an error while streaming the response.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1008706234"] = "The provider '{0}' reported an error while streaming the response."
|
||||
|
||||
-- The provider rejected the request because too many requests were sent. Please wait a moment and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1028424693"] = "The provider rejected the request because too many requests were sent. Please wait a moment and try again."
|
||||
|
||||
-- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding."
|
||||
|
||||
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'"
|
||||
|
||||
@ -6496,6 +6603,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3759732886"] = "We tried to
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T4049517041"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'"
|
||||
|
||||
-- The provider '{0}' reported an error: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T700894460"] = "The provider '{0}' reported an error: {1}"
|
||||
|
||||
-- The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe."
|
||||
|
||||
@ -6556,6 +6666,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected"
|
||||
-- We could not load models from '{0}'. The account or API key does not have the required permissions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T1143085203"] = "We could not load models from '{0}'. The account or API key does not have the required permissions."
|
||||
|
||||
-- We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T155481725"] = "We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again."
|
||||
|
||||
-- We could not load models from '{0}'. The API key is probably missing, invalid, or expired.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2041046579"] = "We could not load models from '{0}'. The API key is probably missing, invalid, or expired."
|
||||
|
||||
@ -6565,15 +6678,39 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T21156887
|
||||
-- We could not load models from '{0}' because the provider returned an unexpected response.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2186844789"] = "We could not load models from '{0}' because the provider returned an unexpected response."
|
||||
|
||||
-- We could not load models from '{0}' because the account appears to have no API credits left.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T373339048"] = "We could not load models from '{0}' because the account appears to have no API credits left."
|
||||
|
||||
-- We could not load models from '{0}' due to an unknown error.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "We could not load models from '{0}' due to an unknown error."
|
||||
|
||||
-- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again."
|
||||
|
||||
-- Model as configured by whisper.cpp
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp"
|
||||
|
||||
-- Cannot export this chat template because example message {0} is not a text message.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Cannot export this chat template because example message {0} is not a text message."
|
||||
|
||||
-- Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2407395493"] = "Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins."
|
||||
|
||||
-- Please select a valid configuration plugin folder. The folder must contain a plugin.lua file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2542895569"] = "Please select a valid configuration plugin folder. The folder must contain a plugin.lua file."
|
||||
|
||||
-- Cannot package the chat template attachments. The issue was: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T3635593138"] = "Cannot package the chat template attachments. The issue was: {0}"
|
||||
|
||||
-- Cannot package the attachment '{0}' because the file does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4121340492"] = "Cannot package the attachment '{0}' because the file does not exist."
|
||||
|
||||
-- Use no chat template
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
|
||||
|
||||
-- Cannot export this chat template because example message {0} is empty.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T477540958"] = "Cannot export this chat template because example message {0} is empty."
|
||||
|
||||
-- Navigation never expands, but there are tooltips
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Navigation never expands, but there are tooltips"
|
||||
|
||||
@ -6769,8 +6906,8 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2708
|
||||
-- Unknown preview feature
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unknown preview feature"
|
||||
|
||||
-- Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T714355911"] = "Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text"
|
||||
-- Transcription: Convert recordings and audio files into text
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T4247148645"] = "Transcription: Convert recordings and audio files into text"
|
||||
|
||||
-- Use no data sources, when sending an assistant result to a chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Use no data sources, when sending an assistant result to a chat"
|
||||
@ -6796,6 +6933,21 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::THEMESEXTENSIONS::T534715610"] =
|
||||
-- Use no profile
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::PROFILE::T2205839602"] = "Use no profile"
|
||||
|
||||
-- The selected model is not available.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T1578005752"] = "The selected model is not available."
|
||||
|
||||
-- The selected provider is not allowed for this chat.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T174545104"] = "The selected provider is not allowed for this chat."
|
||||
|
||||
-- The AI job failed. The message is: '{0}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T237448388"] = "The AI job failed. The message is: '{0}'"
|
||||
|
||||
-- The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T3267850764"] = "The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings."
|
||||
|
||||
-- We could load models from '{0}', but the provider did not return any usable text models.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T3378120620"] = "We could load models from '{0}', but the provider did not return any usable text models."
|
||||
|
||||
-- SSO (Kerberos)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AUTHMETHODSV1EXTENSIONS::T268552140"] = "SSO (Kerberos)"
|
||||
|
||||
@ -6937,6 +7089,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "
|
||||
-- Reason
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NOEMBEDDINGSTORE::T1093747001"] = "Reason"
|
||||
|
||||
-- Starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Starting"
|
||||
|
||||
-- Unavailable
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NOEMBEDDINGSTORE::T3662391977"] = "Unavailable"
|
||||
|
||||
@ -7021,6 +7176,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2858189239"] = "Faile
|
||||
-- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout."
|
||||
|
||||
-- Failed to read the user's username from the operating system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2909734556"] = "Failed to read the user's username from the operating system."
|
||||
|
||||
-- Failed to retrieve the security requirements due to an exception: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Failed to retrieve the security requirements due to an exception: {0}"
|
||||
|
||||
@ -7066,6 +7224,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Failed
|
||||
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
|
||||
|
||||
-- AI Studio couldn't install Pandoc because the archive was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found."
|
||||
|
||||
-- Pandoc doesn't seem to be installed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc doesn't seem to be installed."
|
||||
|
||||
-- Was not able to validate the Pandoc installation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Was not able to validate the Pandoc installation."
|
||||
|
||||
@ -7087,20 +7251,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} is instal
|
||||
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} is installed, but it does not match the required version (v{1})."
|
||||
|
||||
-- Pandoc was not installed successfully, because the archive was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc was not installed successfully, because the archive was not found."
|
||||
-- AI Studio couldn't install Pandoc because the archive type is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio couldn't install Pandoc because the archive type is unknown."
|
||||
|
||||
-- Pandoc is not available on the system or the process had issues.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not available on the system or the process had issues."
|
||||
|
||||
-- Pandoc was not installed successfully, because the archive type is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc was not installed successfully, because the archive type is unknown."
|
||||
-- AI Studio couldn't install Pandoc because the executable was not found in the archive.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio couldn't install Pandoc because the executable was not found in the archive."
|
||||
|
||||
-- It seems that Pandoc is not installed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc is not installed."
|
||||
-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio couldn't find the latest Pandoc version and will install version {0} instead."
|
||||
|
||||
-- The latest Pandoc version was not found, installing version {0} instead.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead."
|
||||
-- AI Studio couldn't install Pandoc.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't install Pandoc."
|
||||
|
||||
-- Pandoc is required for Microsoft Word export.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export."
|
||||
@ -7606,6 +7770,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701
|
||||
-- Pandoc may be required for importing files.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
||||
|
||||
-- Failed to store the secret data due to an API issue.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue."
|
||||
|
||||
-- Failed to delete the secret data due to an API issue.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue."
|
||||
|
||||
@ -7735,6 +7902,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T29806295
|
||||
-- Images are not supported at this place
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T305247150"] = "Images are not supported at this place"
|
||||
|
||||
-- This file format is not supported. Please convert the .doc file to .docx (e.g. with Microsoft Word).
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T3740637731"] = "This file format is not supported. Please convert the .doc file to .docx (e.g. with Microsoft Word)."
|
||||
|
||||
-- Unsupported file type
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T4041351522"] = "Unsupported file type"
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||
@T("You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")
|
||||
</MudJustifiedText>
|
||||
<MudTextField T="string" AutoGrow="true" Lines="3" @bind-Text="@this.importantAspects" class="mb-1" Label="@T("(Optional) Important Aspects")" HelperText="@T("(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start"/>
|
||||
<MudTextField T="string" AutoGrow="true" Lines="3" @bind-Text="@this.importantAspects" class="mb-1" Label="@T("(Optional) Important Aspects")" HelperText="@T("(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
|
||||
<MudText Typo="Typo.h6" Class="mb-1 mt-3"> @T("Extent of the planned presentation")</MudText>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-2">
|
||||
|
||||
@ -93,59 +93,70 @@ public sealed class ContentText : IContent
|
||||
|
||||
// Start another thread by using a task to uncouple
|
||||
// the UI thread from the AI processing:
|
||||
await Task.Run(async () =>
|
||||
try
|
||||
{
|
||||
// We show the waiting animation until we get the first response:
|
||||
this.InitialRemoteWait = true;
|
||||
|
||||
// Iterate over the responses from the AI:
|
||||
await foreach (var contentStreamChunk in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
// When the user cancels the request, we stop the loop:
|
||||
if (token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
// Stop the waiting animation:
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = true;
|
||||
|
||||
// Add the response to the text:
|
||||
this.Text += contentStreamChunk;
|
||||
|
||||
// Merge the sources:
|
||||
this.Sources.MergeSources(contentStreamChunk.Sources);
|
||||
|
||||
// Notify the UI that the content has changed,
|
||||
// depending on the energy saving mode:
|
||||
var now = DateTimeOffset.Now;
|
||||
switch (settings.ConfigurationData.App.IsSavingEnergy)
|
||||
try
|
||||
{
|
||||
// Energy saving mode is off. We notify the UI
|
||||
// as fast as possible -- no matter the odds:
|
||||
case false:
|
||||
await this.StreamingEvent();
|
||||
break;
|
||||
|
||||
// Energy saving mode is on. We notify the UI
|
||||
// only when the time between two events is
|
||||
// greater than the minimum time:
|
||||
case true when now - last > MIN_TIME:
|
||||
last = now;
|
||||
await this.StreamingEvent();
|
||||
break;
|
||||
// We show the waiting animation until we get the first response:
|
||||
this.InitialRemoteWait = true;
|
||||
|
||||
// Iterate over the responses from the AI:
|
||||
await foreach (var contentStreamChunk in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
|
||||
{
|
||||
// When the user cancels the request, we stop the loop:
|
||||
if (token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
// Stop the waiting animation:
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = true;
|
||||
|
||||
// Add the response to the text:
|
||||
this.Text += contentStreamChunk;
|
||||
|
||||
// Merge the sources:
|
||||
this.Sources.MergeSources(contentStreamChunk.Sources);
|
||||
|
||||
// Notify the UI that the content has changed,
|
||||
// depending on the energy saving mode:
|
||||
var now = DateTimeOffset.Now;
|
||||
switch (settings.ConfigurationData.App.IsSavingEnergy)
|
||||
{
|
||||
// Energy saving mode is off. We notify the UI
|
||||
// as fast as possible -- no matter the odds:
|
||||
case false:
|
||||
await this.StreamingEvent();
|
||||
break;
|
||||
|
||||
// Energy saving mode is on. We notify the UI
|
||||
// only when the time between two events is
|
||||
// greater than the minimum time:
|
||||
case true when now - last > MIN_TIME:
|
||||
last = now;
|
||||
await this.StreamingEvent();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the waiting animation (in case the loop
|
||||
// was stopped, or no content was received):
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = false;
|
||||
}, token);
|
||||
|
||||
this.Text = this.Text.RemoveThinkTags().Trim();
|
||||
finally
|
||||
{
|
||||
// Stop the waiting animation (in case the loop
|
||||
// was stopped, or no content was received):
|
||||
this.InitialRemoteWait = false;
|
||||
this.IsStreaming = false;
|
||||
}
|
||||
}, token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.Text = this.Text.RemoveThinkTags().Trim();
|
||||
|
||||
// Inform the UI that the streaming is done:
|
||||
await this.StreamingDone();
|
||||
// Inform the UI that the streaming is done:
|
||||
await this.StreamingDone();
|
||||
}
|
||||
|
||||
return chatThread;
|
||||
}
|
||||
|
||||
|
||||
@ -89,8 +89,10 @@ public static class IImageSourceExtensions
|
||||
|
||||
case ContentImageSource.URL:
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
using var httpClient = ExternalHttpClientTimeout.CreateHttpClient();
|
||||
using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token);
|
||||
var timeoutToken = timeoutTokenSource.Token;
|
||||
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, timeoutToken);
|
||||
if(response.IsSuccessStatusCode)
|
||||
{
|
||||
// Read the length of the content:
|
||||
@ -101,7 +103,7 @@ public static class IImageSourceExtensions
|
||||
return (success: false, string.Empty);
|
||||
}
|
||||
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync(token);
|
||||
var bytes = await response.Content.ReadAsByteArrayAsync(timeoutToken);
|
||||
return (success: true, Convert.ToBase64String(bytes));
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,11 @@ public partial class Changelog
|
||||
|
||||
public static readonly Log[] LOGS =
|
||||
[
|
||||
new (240, "v26.5.5, build 240 (2026-05-25 18:52 UTC)", "v26.5.5.md"),
|
||||
new (239, "v26.5.4, build 239 (2026-05-13 11:58 UTC)", "v26.5.4.md"),
|
||||
new (238, "v26.5.3, build 238 (2026-05-13 09:50 UTC)", "v26.5.3.md"),
|
||||
new (237, "v26.5.2, build 237 (2026-05-06 16:38 UTC)", "v26.5.2.md"),
|
||||
new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"),
|
||||
new (235, "v26.4.1, build 235 (2026-04-17 17:25 UTC)", "v26.4.1.md"),
|
||||
new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"),
|
||||
new (233, "v26.2.1, build 233 (2026-02-01 19:16 UTC)", "v26.2.1.md"),
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
<UserPromptComponent
|
||||
T="string"
|
||||
@ref="@this.inputField"
|
||||
@bind-Text="@this.userInput"
|
||||
@bind-Text="@this.UserInput"
|
||||
Variant="Variant.Outlined"
|
||||
AutoGrow="@true"
|
||||
Lines="3"
|
||||
@ -71,7 +71,7 @@
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
|
||||
{
|
||||
<MudTooltip Text="@T("Save chat")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@(() => this.SaveThread())" Disabled="@(!this.CanThreadBeSaved || this.isStreaming)"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@(() => this.SaveThread())" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@ -92,35 +92,35 @@
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
<MudTooltip Text="@T("Delete this chat & start a new one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="@(() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true))" Disabled="@(!this.CanThreadBeSaved)"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="@(() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true))" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
|
||||
{
|
||||
<MudTooltip Text="@T("Move the chat to a workspace, or to another if it is already in one.")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="@(() => this.MoveChatToWorkspace())"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved || this.IsCurrentChatStreaming)" OnClick="@this.MoveChatToWorkspace"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<AttachDocuments Name="File Attachments" Layer="@DropLayers.PAGES" @bind-DocumentPaths="@this.chatDocumentPaths" CatchAllDocuments="true" UseSmallForm="true" Provider="@this.Provider"/>
|
||||
<AttachDocuments Name="File Attachments" Layer="@DropLayers.PAGES" DocumentPaths="@this.ComposerState.FileAttachments" DocumentPathsChanged="@this.ComposerAttachmentsChanged" CatchAllDocuments="true" UseSmallForm="true" Provider="@this.Provider"/>
|
||||
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
|
||||
<MudTooltip Text="@T("Bold")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatBold" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_BOLD)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatBold" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_BOLD))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Italic")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatItalic" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_ITALIC)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatItalic" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_ITALIC))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Heading")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.TextFields" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_HEADING)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.TextFields" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_HEADING))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Bulleted List")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatListBulleted" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_BULLET_LIST)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FormatListBulleted" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_BULLET_LIST))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="@T("Code")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Code" OnClick="() => this.ApplyMarkdownFormat(MARKDOWN_CODE)" Disabled="@this.IsInputForbidden()"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Code" OnClick="@(() => this.ApplyMarkdownFormat(MARKDOWN_CODE))" Disabled="@this.IsInputForbidden()"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
@ -137,10 +137,10 @@
|
||||
<ConfidenceInfo Mode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
|
||||
}
|
||||
|
||||
@if (this.isStreaming && this.cancellationTokenSource is not null)
|
||||
@if (this.IsCurrentChatStreaming)
|
||||
{
|
||||
<MudTooltip Text="@T("Stop generation")" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="@(() => this.CancelStreaming())"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="@this.CancelStreaming"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ using AIStudio.Dialogs;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
@ -38,6 +38,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
[Parameter]
|
||||
public Workspaces? Workspaces { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public ChatComposerState ComposerState { get; set; } = new();
|
||||
|
||||
[Inject]
|
||||
private ILogger<ChatComponent> Logger { get; set; } = null!;
|
||||
@ -50,6 +53,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
[Inject]
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AIJobService AIJobService { get; init; } = null!;
|
||||
|
||||
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
@ -61,8 +67,6 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
private bool mustScrollToBottomAfterRender;
|
||||
private InnerScrolling scrollingArea = null!;
|
||||
private byte scrollRenderCountdown;
|
||||
private bool isStreaming;
|
||||
private string userInput = string.Empty;
|
||||
private bool mustStoreChat;
|
||||
private bool mustLoadChat;
|
||||
private LoadChat loadChat;
|
||||
@ -70,6 +74,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
private Guid currentWorkspaceId = Guid.Empty;
|
||||
private Guid currentChatThreadId = Guid.Empty;
|
||||
private Guid loadedParameterChatId = Guid.Empty;
|
||||
private Guid loadedParameterWorkspaceId = Guid.Empty;
|
||||
private Guid foregroundChatId = Guid.Empty;
|
||||
private int workspaceHeaderSyncVersion;
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
private HashSet<FileAttachment> chatDocumentPaths = [];
|
||||
@ -80,12 +87,27 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// this, we cannot clear the input field.
|
||||
private UserPromptComponent<string> inputField = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the user's input in the chat interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property serves as a bridge between the chat component and the
|
||||
/// underlying composer state, allowing user input to be dynamically updated
|
||||
/// and managed. The setter also triggers state changes within the composer
|
||||
/// to track whether the user has drafted any input.
|
||||
/// </remarks>
|
||||
private string UserInput
|
||||
{
|
||||
get => this.ComposerState.UserInput;
|
||||
set => this.ComposerState.SetUserInput(value);
|
||||
}
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Apply the filters for the message bus:
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]);
|
||||
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED ]);
|
||||
|
||||
// Configure the spellchecking for the user input:
|
||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||
@ -96,15 +118,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
// Get the preselected chat template:
|
||||
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
|
||||
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
|
||||
if (!this.ComposerState.HasUserDraft && !this.ComposerState.HasComposerContent)
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
|
||||
var deferredInput = MessageBus.INSTANCE.CheckDeferredMessages<string>(Event.SEND_TO_CHAT_INPUT).FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(deferredInput))
|
||||
this.userInput = deferredInput;
|
||||
|
||||
// Apply template's file attachments, if any:
|
||||
foreach (var attachment in this.currentChatTemplate.FileAttachments)
|
||||
this.chatDocumentPaths.Add(attachment.Normalize());
|
||||
this.ComposerState.SetUserInput(deferredInput);
|
||||
|
||||
//
|
||||
// Check for deferred messages of the kind 'SEND_TO_CHAT',
|
||||
@ -122,6 +141,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.ChatThread.IncludeDateTime = true;
|
||||
|
||||
this.Logger.LogInformation($"The chat '{this.ChatThread.ChatId}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now.");
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
|
||||
// We know already that the chat thread is not null,
|
||||
@ -222,6 +242,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
// Select the correct provider:
|
||||
await this.SelectProviderWhenLoadingChat();
|
||||
await this.SyncForegroundChatAsync();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
@ -247,6 +268,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
if(this.ChatThread is not null)
|
||||
{
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
|
||||
|
||||
@ -277,12 +299,35 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
await this.ApplyLoadedChatParameterAsync();
|
||||
await this.SyncForegroundChatAsync();
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task ApplyLoadedChatParameterAsync()
|
||||
{
|
||||
var chatId = this.ChatThread?.ChatId ?? Guid.Empty;
|
||||
var workspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
|
||||
|
||||
if (this.loadedParameterChatId == chatId && this.loadedParameterWorkspaceId == workspaceId)
|
||||
{
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadedParameterChatId = chatId;
|
||||
this.loadedParameterWorkspaceId = workspaceId;
|
||||
await this.LoadedChatChanged(notifyParent: false);
|
||||
}
|
||||
|
||||
private void MarkCurrentChatAsLoadedParameter()
|
||||
{
|
||||
this.loadedParameterChatId = this.ChatThread?.ChatId ?? Guid.Empty;
|
||||
this.loadedParameterWorkspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
|
||||
}
|
||||
|
||||
private async Task SyncWorkspaceHeaderWithChatThreadAsync()
|
||||
{
|
||||
var syncVersion = Interlocked.Increment(ref this.workspaceHeaderSyncVersion);
|
||||
@ -338,7 +383,23 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.WorkspaceName(this.currentWorkspaceName);
|
||||
}
|
||||
|
||||
private async Task SyncForegroundChatAsync()
|
||||
{
|
||||
var nextForegroundChatId = this.ChatThread?.ChatId ?? Guid.Empty;
|
||||
if (this.foregroundChatId == nextForegroundChatId)
|
||||
return;
|
||||
|
||||
if (this.foregroundChatId != Guid.Empty)
|
||||
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, false);
|
||||
|
||||
this.foregroundChatId = nextForegroundChatId;
|
||||
if (this.foregroundChatId != Guid.Empty)
|
||||
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, true);
|
||||
}
|
||||
|
||||
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
|
||||
|
||||
private bool IsCurrentChatStreaming => this.ChatThread is not null && this.AIJobService.IsChatGenerationActive(this.ChatThread.ChatId);
|
||||
|
||||
private string ProviderPlaceholder => this.IsProviderSelected ? T("Type your input here...") : T("Select a provider first");
|
||||
|
||||
@ -408,12 +469,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
{
|
||||
this.currentChatTemplate = chatTemplate;
|
||||
if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt))
|
||||
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
|
||||
this.ComposerState.SetSystemInput(this.currentChatTemplate.PredefinedUserPrompt);
|
||||
|
||||
// Apply template's file attachments (replaces existing):
|
||||
this.chatDocumentPaths.Clear();
|
||||
foreach (var attachment in this.currentChatTemplate.FileAttachments)
|
||||
this.chatDocumentPaths.Add(attachment.Normalize());
|
||||
this.ComposerState.ReplaceFileAttachments(this.currentChatTemplate.FileAttachments);
|
||||
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
@ -458,7 +517,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if (!this.IsProviderSelected)
|
||||
return true;
|
||||
|
||||
if(this.isStreaming)
|
||||
if(this.IsCurrentChatStreaming)
|
||||
return true;
|
||||
|
||||
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
@ -473,6 +532,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.dataSourceSelectionComponent.Hide();
|
||||
|
||||
this.hasUnsavedChanges = true;
|
||||
this.ComposerState.MarkUserDraft();
|
||||
var key = keyEvent.Code.ToLowerInvariant();
|
||||
|
||||
// Was the enter key (either enter or numpad enter) pressed?
|
||||
@ -507,7 +567,16 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(this.dataSourceSelectionComponent?.IsVisible ?? false)
|
||||
this.dataSourceSelectionComponent.Hide();
|
||||
|
||||
this.userInput = await this.JsRuntime.InvokeAsync<string>("formatChatInputMarkdown", CHAT_INPUT_ID, formatType);
|
||||
this.ComposerState.SetUserInput(await this.JsRuntime.InvokeAsync<string>("formatChatInputMarkdown", CHAT_INPUT_ID, formatType));
|
||||
this.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
private void ComposerAttachmentsChanged(HashSet<FileAttachment> attachments)
|
||||
{
|
||||
if (!ReferenceEquals(this.ComposerState.FileAttachments, attachments))
|
||||
this.ComposerState.ReplaceFileAttachments(attachments);
|
||||
|
||||
this.ComposerState.MarkUserDraft();
|
||||
this.hasUnsavedChanges = true;
|
||||
}
|
||||
|
||||
@ -535,17 +604,18 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
WorkspaceId = this.currentWorkspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
DataSourceOptions = this.earlyDataSourceOptions,
|
||||
Name = this.ExtractThreadName(this.userInput),
|
||||
Name = this.ExtractThreadName(this.ComposerState.UserInput),
|
||||
Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(x => x.DeepClone()).ToList(),
|
||||
};
|
||||
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the thread name if it is empty:
|
||||
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
|
||||
this.ChatThread.Name = this.ExtractThreadName(this.userInput);
|
||||
this.ChatThread.Name = this.ExtractThreadName(this.ComposerState.UserInput);
|
||||
|
||||
// Update provider, profile and chat template:
|
||||
this.ChatThread.SelectedProvider = this.Provider.Id;
|
||||
@ -562,14 +632,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
IContent? lastUserPrompt;
|
||||
if (!reuseLastUserPrompt)
|
||||
{
|
||||
var normalizedAttachments = this.chatDocumentPaths
|
||||
var normalizedAttachments = this.ComposerState.FileAttachments
|
||||
.Select(attachment => attachment.Normalize())
|
||||
.Where(attachment => attachment.IsValid)
|
||||
.ToList();
|
||||
|
||||
lastUserPrompt = new ContentText
|
||||
{
|
||||
Text = this.userInput,
|
||||
Text = this.ComposerState.UserInput,
|
||||
FileAttachments = normalizedAttachments,
|
||||
};
|
||||
|
||||
@ -616,14 +686,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// Clear the input field:
|
||||
await this.inputField.FocusAsync();
|
||||
|
||||
this.userInput = string.Empty;
|
||||
this.chatDocumentPaths.Clear();
|
||||
this.ComposerState.Clear();
|
||||
|
||||
await this.inputField.BlurAsync();
|
||||
this.tokenCount = "0";
|
||||
|
||||
// Enable the stream state for the chat component:
|
||||
this.isStreaming = true;
|
||||
this.hasUnsavedChanges = true;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
|
||||
@ -633,38 +701,23 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
}
|
||||
|
||||
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
|
||||
|
||||
using (this.cancellationTokenSource = new())
|
||||
await this.AIJobService.TryStartChatGenerationAsync(new ChatGenerationRequest
|
||||
{
|
||||
this.StateHasChanged();
|
||||
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
this.ChatThread = await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
this.cancellationTokenSource = null;
|
||||
ChatThread = this.ChatThread!,
|
||||
AIText = aiText,
|
||||
LastUserPrompt = lastUserPrompt,
|
||||
ProviderSettings = this.Provider,
|
||||
IsForeground = true,
|
||||
});
|
||||
|
||||
// Save the chat:
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
{
|
||||
await this.SaveThread();
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
// Disable the stream state:
|
||||
this.isStreaming = false;
|
||||
|
||||
// Update the UI:
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task CancelStreaming()
|
||||
{
|
||||
if (this.cancellationTokenSource is not null)
|
||||
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||
await this.cancellationTokenSource.CancelAsync();
|
||||
if (this.ChatThread is not null)
|
||||
await this.AIJobService.CancelChatGenerationAsync(this.ChatThread.ChatId);
|
||||
}
|
||||
|
||||
private async Task SaveThread()
|
||||
@ -694,7 +747,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
// Want the user to manage the chat storage manually? In that case, we have to ask the user
|
||||
// about possible data loss:
|
||||
//
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
@ -727,9 +780,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
//
|
||||
// Reset our state:
|
||||
//
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.ComposerState.Clear();
|
||||
|
||||
//
|
||||
// Reset the LLM provider considering the user's settings:
|
||||
@ -786,17 +838,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
};
|
||||
}
|
||||
|
||||
this.userInput = this.currentChatTemplate.PredefinedUserPrompt;
|
||||
|
||||
// Apply template's file attachments:
|
||||
this.chatDocumentPaths.Clear();
|
||||
foreach (var attachment in this.currentChatTemplate.FileAttachments)
|
||||
this.chatDocumentPaths.Add(attachment.Normalize());
|
||||
this.ComposerState.ApplyTemplate(this.currentChatTemplate);
|
||||
|
||||
// Now, we have to reset the data source options as well:
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
|
||||
// Notify the parent component about the change:
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
|
||||
@ -805,7 +854,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges && !this.IsCurrentChatStreaming)
|
||||
{
|
||||
var confirmationDialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
@ -838,25 +887,35 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
await WorkspaceBehaviour.DeleteChatAsync(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
|
||||
|
||||
this.ChatThread!.WorkspaceId = workspaceId;
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.SaveThread();
|
||||
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
}
|
||||
|
||||
private async Task LoadedChatChanged()
|
||||
private async Task LoadedChatChanged(bool notifyParent = true)
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.ComposerState.Clear();
|
||||
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
this.ChatThread = this.AIJobService.TryGetLiveChatThread(this.ChatThread.ChatId) ?? this.ChatThread;
|
||||
this.loadedParameterChatId = this.ChatThread.ChatId;
|
||||
this.loadedParameterWorkspaceId = this.ChatThread.WorkspaceId;
|
||||
if (notifyParent)
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.loadedParameterChatId = Guid.Empty;
|
||||
this.loadedParameterWorkspaceId = Guid.Empty;
|
||||
this.ClearWorkspaceHeaderState();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
}
|
||||
|
||||
@ -872,12 +931,13 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
private async Task ResetState()
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.hasUnsavedChanges = false;
|
||||
this.userInput = string.Empty;
|
||||
this.ComposerState.Clear();
|
||||
this.ClearWorkspaceHeaderState();
|
||||
|
||||
this.ChatThread = null;
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.SyncForegroundChatAsync();
|
||||
this.ApplyStandardDataSourceOptions();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
}
|
||||
@ -948,7 +1008,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if(lastBlockContent is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.userInput = textBlock.Text;
|
||||
this.RestoreComposerFromTextBlock(textBlock);
|
||||
this.ChatThread.Remove(block);
|
||||
this.ChatThread.Remove(lastBlockContent);
|
||||
this.hasUnsavedChanges = true;
|
||||
@ -965,13 +1025,18 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
if (block is not ContentText textBlock)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.userInput = textBlock.Text;
|
||||
this.RestoreComposerFromTextBlock(textBlock);
|
||||
this.ChatThread.Remove(block);
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void RestoreComposerFromTextBlock(ContentText textBlock)
|
||||
{
|
||||
this.ComposerState.RestoreFromTextBlock(textBlock);
|
||||
}
|
||||
|
||||
private async Task CalculateTokenCount()
|
||||
{
|
||||
@ -1021,8 +1086,17 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
await this.SaveThread();
|
||||
break;
|
||||
|
||||
case Event.WORKSPACE_LOADED_CHAT_CHANGED:
|
||||
await this.LoadedChatChanged();
|
||||
case Event.AI_JOB_CHANGED:
|
||||
case Event.AI_JOB_FINISHED:
|
||||
case Event.CHAT_GENERATION_CHANGED:
|
||||
if (data is AIJobSnapshot { Kind: AIJobKind.CHAT_GENERATION } snapshot && this.ChatThread?.ChatId == snapshot.SubjectId)
|
||||
{
|
||||
this.ChatThread = this.AIJobService.TryGetLiveChatThread(snapshot.SubjectId) ?? this.ChatThread;
|
||||
if (!snapshot.IsActive)
|
||||
this.hasUnsavedChanges = false;
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -1034,8 +1108,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
case Event.HAS_CHAT_UNSAVED_CHANGES:
|
||||
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
return Task.FromResult((TResult?) (object) false);
|
||||
|
||||
if (this.IsCurrentChatStreaming)
|
||||
return Task.FromResult((TResult?) (object) false);
|
||||
|
||||
return Task.FromResult((TResult?)(object)this.hasUnsavedChanges);
|
||||
return Task.FromResult((TResult?)(object)(this.hasUnsavedChanges || this.ComposerState.HasVisibleUserDraft));
|
||||
}
|
||||
|
||||
return Task.FromResult(default(TResult));
|
||||
@ -1053,21 +1130,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
this.hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
if (this.cancellationTokenSource is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(!this.cancellationTokenSource.IsCancellationRequested)
|
||||
await this.cancellationTokenSource.CancelAsync();
|
||||
|
||||
this.cancellationTokenSource.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
await this.AIJobService.SetForegroundAsync(AIJobKind.CHAT_GENERATION, this.foregroundChatId, false);
|
||||
this.Dispose();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
65
app/MindWork AI Studio/Components/ChatComposerState.cs
Normal file
65
app/MindWork AI Studio/Components/ChatComposerState.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public sealed class ChatComposerState
|
||||
{
|
||||
public string UserInput { get; private set; } = string.Empty;
|
||||
|
||||
public HashSet<FileAttachment> FileAttachments { get; } = [];
|
||||
|
||||
public bool HasUserDraft { get; private set; }
|
||||
|
||||
public bool HasComposerContent => !string.IsNullOrWhiteSpace(this.UserInput) || this.FileAttachments.Count > 0;
|
||||
|
||||
public bool HasVisibleUserDraft => this.HasUserDraft && (!string.IsNullOrWhiteSpace(this.UserInput) || this.FileAttachments.Count > 0);
|
||||
|
||||
public void ApplyTemplate(ChatTemplate chatTemplate)
|
||||
{
|
||||
this.UserInput = chatTemplate.PredefinedUserPrompt;
|
||||
this.FileAttachments.Clear();
|
||||
foreach (var attachment in chatTemplate.FileAttachments)
|
||||
this.FileAttachments.Add(attachment.Normalize());
|
||||
|
||||
this.HasUserDraft = false;
|
||||
}
|
||||
|
||||
public void SetUserInput(string? userInput)
|
||||
{
|
||||
this.UserInput = userInput ?? string.Empty;
|
||||
this.HasUserDraft = !string.IsNullOrWhiteSpace(userInput);
|
||||
}
|
||||
|
||||
public void SetSystemInput(string? userInput)
|
||||
{
|
||||
this.UserInput = userInput ?? string.Empty;
|
||||
this.HasUserDraft = false;
|
||||
}
|
||||
|
||||
public void MarkUserDraft()
|
||||
{
|
||||
this.HasUserDraft = true;
|
||||
}
|
||||
|
||||
public void ReplaceFileAttachments(IEnumerable<FileAttachment> fileAttachments)
|
||||
{
|
||||
this.FileAttachments.Clear();
|
||||
foreach (var attachment in fileAttachments)
|
||||
this.FileAttachments.Add(attachment.Normalize());
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
this.UserInput = string.Empty;
|
||||
this.FileAttachments.Clear();
|
||||
this.HasUserDraft = false;
|
||||
}
|
||||
|
||||
public void RestoreFromTextBlock(ContentText textBlock)
|
||||
{
|
||||
this.UserInput = textBlock.Text;
|
||||
this.ReplaceFileAttachments(textBlock.FileAttachments);
|
||||
this.HasUserDraft = true;
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@
|
||||
<ConfigurationSelect OptionDescription="@T("Color theme")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreferredTheme)" Data="@ConfigurationSelectDataFactory.GetThemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreferredTheme = selectedValue)" OptionHelp="@T("Choose the color theme that best suits for you.")"/>
|
||||
<ConfigurationOption OptionDescription="@T("Save energy?")" LabelOn="@T("Energy saving is enabled")" LabelOff="@T("Energy saving is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="@T("When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available.")"/>
|
||||
<ConfigurationOption OptionDescription="@T("Enable spellchecking?")" LabelOn="@T("Spellchecking is enabled")" LabelOff="@T("Spellchecking is disabled")" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="@T("When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.")"/>
|
||||
<ConfigurationSlider T="int" OptionDescription="@T("Request timeout")" Min="@ExternalHttpClientTimeout.MIN_HTTP_CLIENT_TIMEOUT_SECONDS" Max="@ExternalHttpClientTimeout.MAX_HTTP_CLIENT_TIMEOUT_SECONDS" Step="60" Unit="@T("seconds")" Value="@(() => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.App.HttpClientTimeoutSeconds = updatedValue)" OptionHelp="@T("How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.HttpClientTimeoutSeconds, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Check for updates")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInterval)" Data="@ConfigurationSelectDataFactory.GetUpdateIntervalData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInterval = selectedValue)" OptionHelp="@T("How often should we check for app updates?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInterval, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Update installation method")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateInstallation)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviourData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateInstallation = selectedValue)" OptionHelp="@T("Should updates be installed automatically or manually?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UpdateInstallation, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Navigation bar behavior")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="@T("Select the desired behavior for the navigation bar.")"/>
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.VoiceChat" HeaderText="@T("Configure Transcription Providers")">
|
||||
<PreviewBeta ApplyInnerScrollingFix="true"/>
|
||||
<MudText Typo="Typo.h4" Class="mb-3">
|
||||
@T("Configured Transcription Providers")
|
||||
</MudText>
|
||||
|
||||
@ -12,10 +12,16 @@ public class TreeItemData : ITreeItem
|
||||
|
||||
public string Icon { get; init; } = string.Empty;
|
||||
|
||||
public string DefaultIcon { get; init; } = string.Empty;
|
||||
|
||||
public TreeItemType Type { get; init; }
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public Guid ChatId { get; init; }
|
||||
|
||||
public Guid WorkspaceId { get; init; }
|
||||
|
||||
public bool Expandable { get; init; } = true;
|
||||
|
||||
public DateTimeOffset LastEditTime { get; init; }
|
||||
|
||||
@ -132,6 +132,7 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
}
|
||||
|
||||
var mimeTypes = GetPreferredMimeTypes(
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.WEBM).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.OGG).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.AAC).Build(),
|
||||
Builder.Create().UseAudio().UseSubtype(AudioSubtype.MP3).Build(),
|
||||
@ -361,7 +362,18 @@ public partial class VoiceRecorder : MSGComponentBase
|
||||
|
||||
// Call the transcription API:
|
||||
this.Logger.LogInformation("Starting transcription with provider '{ProviderName}' and model '{ModelName}'.", transcriptionProviderSettings.UsedLLMProvider, transcriptionProviderSettings.Model.ToString());
|
||||
var transcribedText = await provider.TranscribeAudioAsync(transcriptionProviderSettings.Model, this.finalRecordingPath, this.SettingsManager);
|
||||
var transcriptionResult = await provider.TranscribeAudioAsync(transcriptionProviderSettings.Model, this.finalRecordingPath, this.SettingsManager);
|
||||
if (!transcriptionResult.Success)
|
||||
{
|
||||
this.Logger.LogWarning("The transcription request failed.");
|
||||
var userMessage = string.IsNullOrWhiteSpace(transcriptionResult.ErrorMessage)
|
||||
? this.T("Unfortunately, there was an error communicating with the AI system.")
|
||||
: transcriptionResult.ErrorMessage;
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.VoiceChat, userMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
var transcribedText = transcriptionResult.Text;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(transcribedText))
|
||||
{
|
||||
|
||||
@ -24,7 +24,7 @@ else
|
||||
case TreeItemData treeItem:
|
||||
@if (treeItem.Type is TreeItemType.LOADING)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@false" Items="@(treeItem.Children!)">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@false" Items="@(treeItem.Children!)">
|
||||
<BodyContent>
|
||||
<MudSkeleton Width="85%" Height="22px"/>
|
||||
</BodyContent>
|
||||
@ -32,7 +32,7 @@ else
|
||||
}
|
||||
else if (treeItem.Type is TreeItemType.CHAT)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.LoadChatAsync(treeItem.Path, true))">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.LoadChatAsync(treeItem.Path, true))">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@ -48,15 +48,15 @@ else
|
||||
<div style="justify-self: end;">
|
||||
|
||||
<MudTooltip Text="@T("Move to workspace")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.MoveChatAsync(treeItem.Path))"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Size="Size.Medium" Color="Color.Inherit" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.MoveChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Rename")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" OnClick="@(() => this.RenameChatAsync(treeItem.Path))"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.RenameChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
|
||||
<MudTooltip Text="@T("Delete")" Placement="@WORKSPACE_ITEM_TOOLTIP_PLACEMENT">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" OnClick="@(() => this.DeleteChatAsync(treeItem.Path))"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Error" Disabled="@this.IsChatTreeItemBusy(treeItem)" OnClick="@(() => this.DeleteChatAsync(treeItem.Path))"/>
|
||||
</MudTooltip>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,7 +65,7 @@ else
|
||||
}
|
||||
else if (treeItem.Type is TreeItemType.WORKSPACE)
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.OnWorkspaceClicked(treeItem))">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)" OnClick="@(() => this.OnWorkspaceClicked(treeItem))">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
@ -86,7 +86,7 @@ else
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@treeItem.Icon" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)">
|
||||
<MudTreeViewItem T="ITreeItem" Icon="@this.GetTreeItemIcon(treeItem)" Value="@item.Value" Expanded="@item.Expanded" CanExpand="@treeItem.Expandable" Items="@(treeItem.Children!)">
|
||||
<BodyContent>
|
||||
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
|
||||
<MudText Style="justify-self: start;">
|
||||
|
||||
@ -4,6 +4,7 @@ using System.Text.Json;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@ -18,6 +19,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
|
||||
[Inject]
|
||||
private ILogger<Workspaces> Logger { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AIJobService AIJobService { get; init; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public ChatThread? CurrentChatThread { get; set; }
|
||||
@ -42,6 +46,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
this.ApplyFilters([], [ Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED, Event.CHAT_GENERATION_CHANGED ]);
|
||||
_ = this.LoadTreeItemsAsync(startPrefetch: true);
|
||||
}
|
||||
|
||||
@ -111,7 +116,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
|
||||
var temporaryChatsChildren = new List<TreeItemData<ITreeItem>>();
|
||||
foreach (var temporaryChat in snapshot.TemporaryChats.OrderByDescending(x => x.LastEditTime))
|
||||
temporaryChatsChildren.Add(CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
|
||||
temporaryChatsChildren.Add(this.CreateChatTreeItem(temporaryChat, WorkspaceBranch.TEMPORARY_CHATS, depth: 1, icon: Icons.Material.Filled.Timer));
|
||||
|
||||
this.treeItems.Add(new TreeItemData<ITreeItem>
|
||||
{
|
||||
@ -136,7 +141,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
if (workspace.ChatsLoaded)
|
||||
{
|
||||
foreach (var workspaceChat in workspace.Chats.OrderByDescending(x => x.LastEditTime))
|
||||
children.Add(CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
|
||||
children.Add(this.CreateChatTreeItem(workspaceChat, WorkspaceBranch.WORKSPACES, depth: 2, icon: Icons.Material.Filled.Chat));
|
||||
}
|
||||
else if (this.loadingWorkspaceChatLists.Contains(workspace.WorkspaceId))
|
||||
children.AddRange(this.CreateLoadingRows(workspace.WorkspacePath));
|
||||
@ -192,7 +197,7 @@ public partial class Workspaces : MSGComponentBase
|
||||
};
|
||||
}
|
||||
|
||||
private static TreeItemData<ITreeItem> CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon)
|
||||
private TreeItemData<ITreeItem> CreateChatTreeItem(WorkspaceTreeChat chat, WorkspaceBranch branch, int depth, string icon)
|
||||
{
|
||||
return new TreeItemData<ITreeItem>
|
||||
{
|
||||
@ -204,13 +209,44 @@ public partial class Workspaces : MSGComponentBase
|
||||
Branch = branch,
|
||||
Text = chat.Name,
|
||||
Icon = icon,
|
||||
DefaultIcon = icon,
|
||||
Expandable = false,
|
||||
Path = chat.ChatPath,
|
||||
ChatId = chat.ChatId,
|
||||
WorkspaceId = chat.WorkspaceId,
|
||||
LastEditTime = chat.LastEditTime,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private string GetTreeItemIcon(TreeItemData treeItem)
|
||||
{
|
||||
if (treeItem.Type is not TreeItemType.CHAT)
|
||||
return treeItem.Icon;
|
||||
|
||||
var defaultIcon = string.IsNullOrWhiteSpace(treeItem.DefaultIcon) ? treeItem.Icon : treeItem.DefaultIcon;
|
||||
return this.GetChatTreeIcon(treeItem.ChatId, defaultIcon);
|
||||
}
|
||||
|
||||
private bool IsChatTreeItemBusy(TreeItemData treeItem)
|
||||
{
|
||||
return treeItem.Type is TreeItemType.CHAT && this.AIJobService.IsChatGenerationActive(treeItem.ChatId);
|
||||
}
|
||||
|
||||
private string GetChatTreeIcon(Guid chatId, string defaultIcon)
|
||||
{
|
||||
var snapshot = this.AIJobService.TryGetChatSnapshot(chatId);
|
||||
if (snapshot is null || !snapshot.IsActive)
|
||||
return defaultIcon;
|
||||
|
||||
return snapshot.Status switch
|
||||
{
|
||||
AIJobStatus.WAITING_FOR_REMOTE => Icons.Material.Filled.HourglassTop,
|
||||
AIJobStatus.RUNNING => Icons.Material.Filled.ChangeCircle,
|
||||
_ => defaultIcon,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task SafeStateHasChanged()
|
||||
{
|
||||
if (this.isDisposed)
|
||||
@ -348,11 +384,13 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
var chatData = await File.ReadAllTextAsync(Path.Join(chatPath, "thread.json"), Encoding.UTF8);
|
||||
var chat = JsonSerializer.Deserialize<ChatThread>(chatData, WorkspaceBehaviour.JSON_OPTIONS);
|
||||
if (chat is not null)
|
||||
chat = this.AIJobService.TryGetLiveChatThread(chat.ChatId) ?? chat;
|
||||
|
||||
if (switchToChat)
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
return chat;
|
||||
@ -371,6 +409,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
return;
|
||||
|
||||
if (askForConfirmation)
|
||||
{
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(chat.WorkspaceId);
|
||||
@ -398,7 +439,6 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
this.CurrentChatThread = null;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -407,6 +447,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<SingleInputDialog>
|
||||
{
|
||||
@ -429,7 +472,6 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
this.CurrentChatThread.Name = chat.Name;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
await WorkspaceBehaviour.StoreChatAsync(chat);
|
||||
@ -525,6 +567,9 @@ public partial class Workspaces : MSGComponentBase
|
||||
var chat = await this.LoadChatAsync(chatPath, false);
|
||||
if (chat is null)
|
||||
return;
|
||||
|
||||
if (this.AIJobService.IsChatGenerationActive(chat.ChatId))
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<WorkspaceSelectionDialog>
|
||||
{
|
||||
@ -549,7 +594,6 @@ public partial class Workspaces : MSGComponentBase
|
||||
{
|
||||
this.CurrentChatThread = chat;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_LOADED_CHAT_CHANGED);
|
||||
}
|
||||
|
||||
await WorkspaceBehaviour.StoreChatAsync(chat);
|
||||
@ -597,6 +641,12 @@ public partial class Workspaces : MSGComponentBase
|
||||
case Event.PLUGINS_RELOADED:
|
||||
await this.ForceRefreshFromDiskAsync();
|
||||
break;
|
||||
|
||||
case Event.AI_JOB_CHANGED:
|
||||
case Event.AI_JOB_FINISHED:
|
||||
case Event.CHAT_GENERATION_CHANGED:
|
||||
await this.SafeStateHasChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudText Typo="Typo.body1" Class="mb-3">
|
||||
@string.Format(T("How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?"), this.DataSource.Name)
|
||||
</MudText>
|
||||
|
||||
<MudSelect @bind-Value="@this.usernamePasswordMode" Text="@this.GetUsernamePasswordModeText()" Label="@T("Username and password mode")" Class="mt-3 mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start">
|
||||
@foreach (var mode in this.availableUsernamePasswordModes)
|
||||
{
|
||||
<MudSelectItem Value="@mode">
|
||||
@this.GetUsernamePasswordModeText(mode)
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
|
||||
@T("Cancel")
|
||||
</MudButton>
|
||||
<MudButton OnClick="@this.Export" Variant="Variant.Filled" Color="Color.Primary">
|
||||
@T("Export")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@ -0,0 +1,37 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class DataSourceERIV1UsernamePasswordExportDialog : MSGComponentBase
|
||||
{
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public DataSourceERI_V1 DataSource { get; set; }
|
||||
|
||||
private readonly DataSourceERIUsernamePasswordMode[] availableUsernamePasswordModes =
|
||||
[
|
||||
DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD,
|
||||
DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD
|
||||
];
|
||||
|
||||
private DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD;
|
||||
|
||||
private string GetUsernamePasswordModeText() => this.GetUsernamePasswordModeText(this.usernamePasswordMode);
|
||||
|
||||
private string GetUsernamePasswordModeText(DataSourceERIUsernamePasswordMode mode) => mode switch
|
||||
{
|
||||
DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD => T("Read each user's username from the operating system and share one password"),
|
||||
DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD => T("Use the same username and password for all users"),
|
||||
|
||||
_ => T("User-managed username and password"),
|
||||
};
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private void Export() => this.MudDialog.Close(DialogResult.Ok(new DataSourceERIV1UsernamePasswordExportDialogResult(this.usernamePasswordMode)));
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public readonly record struct DataSourceERIV1UsernamePasswordExportDialogResult(DataSourceERIUsernamePasswordMode UsernamePasswordMode);
|
||||
@ -116,7 +116,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
||||
if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD)
|
||||
{
|
||||
// Load the secret:
|
||||
var requestedSecret = await this.RustService.GetSecret(this);
|
||||
var requestedSecret = await this.RustService.GetSecret(this, SecretStoreType.DATA_SOURCE);
|
||||
if (requestedSecret.Success)
|
||||
this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption);
|
||||
else
|
||||
@ -169,6 +169,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
||||
Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname,
|
||||
AuthMethod = this.dataAuthMethod,
|
||||
Username = this.dataUsername,
|
||||
UsernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED,
|
||||
Type = DataSourceType.ERI_V1,
|
||||
SecurityPolicy = this.dataSecurityPolicy,
|
||||
SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id,
|
||||
@ -323,7 +324,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
|
||||
if (!string.IsNullOrWhiteSpace(this.dataSecret))
|
||||
{
|
||||
// Store the secret in the OS secure storage:
|
||||
var storeResponse = await this.RustService.SetSecret(this, this.dataSecret);
|
||||
var storeResponse = await this.RustService.SetSecret(this, this.dataSecret, SecretStoreType.DATA_SOURCE);
|
||||
if (!storeResponse.Success)
|
||||
{
|
||||
this.dataSecretStorageIssue = string.Format(T("Failed to store the auth. secret in the operating system. The message was: {0}. Please try again."), storeResponse.Issue);
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
@if (this.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD)
|
||||
{
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.Person2" Label="@T("Username")" Value="@this.DataSource.Username" ClipboardTooltipSubject="@T("the username")"/>
|
||||
<TextInfoLine Icon="@Icons.Material.Filled.Person2" Label="@T("Username")" Value="@this.effectiveUsername" ClipboardTooltipSubject="@T("the username")"/>
|
||||
}
|
||||
|
||||
<TextInfoLines Label="@T("Server description")" MaxLines="14" Value="@this.serverDescription" ClipboardTooltipSubject="@T("the server description")"/>
|
||||
|
||||
@ -41,6 +41,7 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
|
||||
private readonly List<string> dataIssues = [];
|
||||
|
||||
private string serverDescription = string.Empty;
|
||||
private string effectiveUsername = string.Empty;
|
||||
private ProviderType securityRequirements = ProviderType.NONE;
|
||||
private IReadOnlyList<RetrievalInfo> retrievalInfoformation = [];
|
||||
private RetrievalInfo selectedRetrievalInfo;
|
||||
@ -51,6 +52,27 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
|
||||
|
||||
private string Port => this.DataSource.Port == 0 ? string.Empty : $"{this.DataSource.Port}";
|
||||
|
||||
private async Task<(bool Success, DataSourceERI_V1 EffectiveDataSource)> CreateEffectiveDataSource()
|
||||
{
|
||||
this.effectiveUsername = this.DataSource.Username;
|
||||
if (this.DataSource is not { AuthMethod: AuthMethod.USERNAME_PASSWORD, UsernamePasswordMode: DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD })
|
||||
return (true, this.DataSource);
|
||||
|
||||
var osUsername = await this.RustService.ReadUserName();
|
||||
if (string.IsNullOrWhiteSpace(osUsername))
|
||||
{
|
||||
this.dataIssues.Add(T("Failed to read the user's username from the operating system."));
|
||||
return (false, this.DataSource);
|
||||
}
|
||||
|
||||
this.effectiveUsername = osUsername;
|
||||
return (true, this.DataSource with
|
||||
{
|
||||
Username = osUsername,
|
||||
UsernamePasswordMode = DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD,
|
||||
});
|
||||
}
|
||||
|
||||
private string RetrievalName(RetrievalInfo retrievalInfo)
|
||||
{
|
||||
var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id);
|
||||
@ -91,15 +113,19 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos
|
||||
{
|
||||
this.IsOperationInProgress = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
var effectiveDataSourceResult = await this.CreateEffectiveDataSource();
|
||||
if (!effectiveDataSourceResult.Success)
|
||||
return;
|
||||
|
||||
using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource);
|
||||
using var client = ERIClientFactory.Get(ERIVersion.V1, effectiveDataSourceResult.EffectiveDataSource);
|
||||
if(client is null)
|
||||
{
|
||||
this.dataIssues.Add(T("Failed to connect to the ERI v1 server. The server is not supported."));
|
||||
return;
|
||||
}
|
||||
|
||||
var loginResult = await client.AuthenticateAsync(this.RustService);
|
||||
var loginResult = await client.AuthenticateAsync(this.RustService, cancellationToken: this.cts.Token);
|
||||
if (!loginResult.Successful)
|
||||
{
|
||||
this.dataIssues.Add(loginResult.Message);
|
||||
|
||||
@ -18,6 +18,9 @@ public abstract class SettingsDialogBase : MSGComponentBase
|
||||
|
||||
[Inject]
|
||||
protected RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected ISnackbar Snackbar { get; init; } = null!;
|
||||
|
||||
protected readonly List<ConfigurationSelectData<string>> AvailableLLMProviders = new();
|
||||
protected readonly List<ConfigurationSelectData<string>> AvailableEmbeddingProviders = new();
|
||||
|
||||
@ -43,6 +43,28 @@
|
||||
<MudTooltip Text="@T("Edit")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditChatTemplate(context))"/>
|
||||
</MudTooltip>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
{
|
||||
@if (context.FileAttachments.Count == 0)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Dataset" Color="Color.Info" Variant="Variant.Text">
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))">
|
||||
@T("Use shared attachment paths")
|
||||
</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Folder" OnClick="@(() => this.ExportChatTemplateWithPackagedAttachments(context))">
|
||||
@T("Copy attachments into plugin")
|
||||
</MudMenuItem>
|
||||
</MudMenu>
|
||||
</MudTooltip>
|
||||
}
|
||||
}
|
||||
<MudTooltip Text="@T("Delete")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteChatTemplate(context))"/>
|
||||
</MudTooltip>
|
||||
|
||||
@ -98,4 +98,66 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
|
||||
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task ExportChatTemplateWithSharedAttachmentPaths(ChatTemplate chatTemplate)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
|
||||
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
await this.CopyChatTemplateLuaToClipboard(chatTemplate);
|
||||
}
|
||||
|
||||
private async Task ExportChatTemplateWithPackagedAttachments(ChatTemplate chatTemplate)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
|
||||
if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
if (chatTemplate.FileAttachments.Count == 0)
|
||||
{
|
||||
await this.ExportChatTemplateWithSharedAttachmentPaths(chatTemplate);
|
||||
return;
|
||||
}
|
||||
|
||||
var pluginDirectoryResponse = await this.RustService.SelectDirectory(T("Select configuration plugin folder"));
|
||||
if (pluginDirectoryResponse.UserCancelled)
|
||||
return;
|
||||
|
||||
await this.CopyPackagedChatTemplateLuaToClipboard(chatTemplate, pluginDirectoryResponse.SelectedDirectory);
|
||||
}
|
||||
|
||||
private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate)
|
||||
{
|
||||
if (!chatTemplate.TryExportAsConfigurationSection(out var luaCode, out var issue))
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export Chat Template"),
|
||||
issue,
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(luaCode))
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
|
||||
}
|
||||
|
||||
private async Task CopyPackagedChatTemplateLuaToClipboard(ChatTemplate chatTemplate, string pluginDirectory)
|
||||
{
|
||||
if (!chatTemplate.TryExportAsConfigurationSectionWithPackagedAttachments(pluginDirectory, out var luaCode, out var issue))
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export Chat Template"),
|
||||
issue,
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(luaCode))
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
|
||||
}
|
||||
}
|
||||
@ -45,15 +45,30 @@
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
|
||||
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Info" OnClick="() => this.ShowInformation(context)"/>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Sync" Disabled="@(!this.CanRefreshDataSource(context))" OnClick="() => this.RefreshDataSource(context)">
|
||||
@T("Refresh")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)">
|
||||
@T("Edit")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)">
|
||||
@T("Delete")
|
||||
</MudButton>
|
||||
@if (context.IsEnterpriseConfiguration)
|
||||
{
|
||||
<MudTooltip Text="@T("This data source is managed by your organization.")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)">
|
||||
@T("Edit")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Sync" Disabled="@(!this.CanRefreshDataSource(context))" OnClick="() => this.RefreshDataSource(context)">
|
||||
@T("Refresh")
|
||||
</MudButton>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings && context is DataSourceERI_V1)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportDataSource(context)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)">
|
||||
@T("Delete")
|
||||
</MudButton>
|
||||
}
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Dialogs.Settings;
|
||||
|
||||
@ -126,9 +124,106 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
|
||||
await this.DataSourceEmbeddingService.QueueDataSourceAsync(addedDataSource);
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task ExportDataSource(IDataSource dataSource)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
|
||||
if (dataSource is not DataSourceERI_V1 eriDataSource)
|
||||
return;
|
||||
|
||||
if (eriDataSource.AuthMethod is AuthMethod.KERBEROS)
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export ERI Data Source"),
|
||||
T("Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."),
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
var needsSecret = eriDataSource.AuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD;
|
||||
if (!needsSecret)
|
||||
{
|
||||
var publicLuaCode = eriDataSource.ExportAsConfigurationSection();
|
||||
if (!string.IsNullOrWhiteSpace(publicLuaCode))
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, publicLuaCode);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var secretResponse = await this.RustService.GetSecret(eriDataSource, SecretStoreType.DATA_SOURCE, isTrying: true);
|
||||
if (!secretResponse.Success)
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export ERI Data Source"),
|
||||
string.Format(T("Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"), secretResponse.Issue),
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
var encryption = PluginFactory.EnterpriseEncryption;
|
||||
if (encryption?.IsAvailable != true)
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export ERI Data Source"),
|
||||
T("Cannot export this ERI data source because no enterprise encryption secret is configured."),
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
var usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED;
|
||||
if (eriDataSource.AuthMethod is AuthMethod.TOKEN)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, T("This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token.") },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Export Access Token?"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
}
|
||||
else if (eriDataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<DataSourceERIV1UsernamePasswordExportDialog>
|
||||
{
|
||||
{ x => x.DataSource, eriDataSource },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<DataSourceERIV1UsernamePasswordExportDialog>(T("Export ERI Data Source"), dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is not DataSourceERIV1UsernamePasswordExportDialogResult exportResult)
|
||||
return;
|
||||
|
||||
usernamePasswordMode = exportResult.UsernamePasswordMode;
|
||||
}
|
||||
|
||||
var decryptedSecret = await secretResponse.Secret.Decrypt(Program.ENCRYPTION);
|
||||
if (!encryption.TryEncrypt(decryptedSecret, out var encryptedSecret))
|
||||
{
|
||||
await this.DialogService.ShowMessageBox(
|
||||
T("Export ERI Data Source"),
|
||||
T("Cannot export this ERI data source because the authentication secret could not be encrypted."),
|
||||
T("Close"));
|
||||
return;
|
||||
}
|
||||
|
||||
var luaCode = eriDataSource.ExportAsConfigurationSection(
|
||||
encryptedSecret,
|
||||
usernamePasswordMode);
|
||||
if (string.IsNullOrWhiteSpace(luaCode))
|
||||
return;
|
||||
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
|
||||
}
|
||||
|
||||
private async Task EditDataSource(IDataSource dataSource)
|
||||
{
|
||||
if (dataSource.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
IDataSource? editedDataSource = null;
|
||||
switch (dataSource)
|
||||
{
|
||||
@ -192,6 +287,9 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
|
||||
|
||||
private async Task DeleteDataSource(IDataSource dataSource)
|
||||
{
|
||||
if (dataSource.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
{ x => x.Message, string.Format(T("Are you sure you want to delete the data source '{0}' of type {1}?"), dataSource.Name, dataSource.Type.GetDisplayName()) },
|
||||
@ -215,7 +313,7 @@ public partial class SettingsDialogDataSources : SettingsDialogBase
|
||||
// All other auth methods require a secret, which we need to delete now:
|
||||
else
|
||||
{
|
||||
var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource);
|
||||
var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource, SecretStoreType.DATA_SOURCE);
|
||||
if (deleteSecretResponse.Success)
|
||||
applyChanges = true;
|
||||
}
|
||||
|
||||
@ -42,6 +42,12 @@
|
||||
<MudTooltip Text="@T("Edit")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditProfile(context)"/>
|
||||
</MudTooltip>
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
{
|
||||
<MudTooltip Text="@T("Export configuration")">
|
||||
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportProfile(context)"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudTooltip Text="@T("Delete")">
|
||||
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteProfile(context)"/>
|
||||
</MudTooltip>
|
||||
|
||||
@ -49,6 +49,19 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
private async Task ExportProfile(Profile profile)
|
||||
{
|
||||
if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
|
||||
return;
|
||||
|
||||
if (profile == Profile.NO_PROFILE || profile.IsEnterpriseConfiguration)
|
||||
return;
|
||||
|
||||
var luaCode = profile.ExportAsConfigurationSection();
|
||||
if (!string.IsNullOrWhiteSpace(luaCode))
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode);
|
||||
}
|
||||
|
||||
private async Task DeleteProfile(Profile profile)
|
||||
{
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
<MudNavMenu>
|
||||
@foreach (var navBarItem in this.navItems)
|
||||
{
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color">
|
||||
<MudNavLink Href="@navBarItem.Path" Match="@(navBarItem.MatchAll ? NavLinkMatch.All : NavLinkMatch.Prefix)" Icon="@navBarItem.Icon" Style="@navBarItem.SetColorStyle(this.SettingsManager)" Class="custom-icon-color">
|
||||
@navBarItem.Name
|
||||
</MudNavLink>
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ using System.Runtime.CompilerServices;
|
||||
using AIStudio.Dialogs;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
@ -27,6 +28,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private AIJobService AIJobService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ISnackbar Snackbar { get; init; } = null!;
|
||||
@ -90,7 +94,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
// Read the user language from Rust:
|
||||
//
|
||||
var userLanguage = await this.RustService.ReadUserLanguage();
|
||||
var userName = await this.RustService.ReadUserName();
|
||||
this.Logger.LogInformation($"The OS says '{userLanguage}' is the user language.");
|
||||
this.Logger.LogInformation($"The OS says '{userName}' is the username.");
|
||||
|
||||
// Ensure that all settings are loaded:
|
||||
await this.SettingsManager.LoadSettings();
|
||||
@ -102,7 +108,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
[
|
||||
Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR,
|
||||
Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED,
|
||||
Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED, Event.RAG_EMBEDDING_STATUS_CHANGED,
|
||||
Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED,
|
||||
Event.CHAT_GENERATION_CHANGED, Event.RAG_EMBEDDING_STATUS_CHANGED,
|
||||
]);
|
||||
|
||||
// Set the snackbar for the update service:
|
||||
@ -194,6 +201,13 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.AI_JOB_CHANGED:
|
||||
case Event.AI_JOB_FINISHED:
|
||||
case Event.CHAT_GENERATION_CHANGED:
|
||||
this.LoadNavItems();
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.SHOW_SUCCESS:
|
||||
if (data is DataSuccessMessage success)
|
||||
success.Show(this.Snackbar);
|
||||
@ -311,7 +325,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
|
||||
|
||||
yield return new(T("Home"), Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true);
|
||||
yield return new(T("Chat"), Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false);
|
||||
yield return new(T("Chat"), this.AIJobService.HasActiveJobs ? Icons.Material.Filled.Chat : Icons.Material.Outlined.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false);
|
||||
yield return new(T("Assistants"), Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false);
|
||||
|
||||
if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager))
|
||||
|
||||
@ -50,12 +50,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.3.0" />
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.15" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.16" />
|
||||
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
||||
<PackageReference Include="MudBlazor.Markdown" Version="8.11.0" />
|
||||
<PackageReference Include="Qdrant.Client" Version="1.17.0" />
|
||||
<PackageReference Include="Qdrant.Client" Version="1.18.1" />
|
||||
<PackageReference Include="ReverseMarkdown" Version="5.0.0" />
|
||||
<PackageReference Include="LuaCSharp" Version="0.5.3" />
|
||||
<PackageReference Include="LuaCSharp" Version="0.5.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -89,6 +89,7 @@
|
||||
<ChatComponent
|
||||
@bind-ChatThread="@this.chatThread"
|
||||
@bind-Provider="@this.providerSettings"
|
||||
ComposerState="@this.composerState"
|
||||
Workspaces="@this.workspaces"
|
||||
WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
|
||||
</EndContent>
|
||||
@ -115,6 +116,7 @@
|
||||
<ChatComponent
|
||||
@bind-ChatThread="@this.chatThread"
|
||||
@bind-Provider="@this.providerSettings"
|
||||
ComposerState="@this.composerState"
|
||||
Workspaces="@this.workspaces"
|
||||
WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
|
||||
</MudStack>
|
||||
@ -125,6 +127,7 @@
|
||||
<ChatComponent
|
||||
@bind-ChatThread="@this.chatThread"
|
||||
@bind-Provider="@this.providerSettings"
|
||||
ComposerState="@this.composerState"
|
||||
Workspaces="@this.workspaces"
|
||||
WorkspaceName="name => this.UpdateWorkspaceName(name)"/>
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ public partial class Chat : MSGComponentBase
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
private Workspaces? workspaces;
|
||||
private double splitterPosition = 30;
|
||||
private readonly ChatComposerState composerState = new();
|
||||
|
||||
private readonly Timer splitterSaveTimer = new(TimeSpan.FromSeconds(1.6));
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Translate" Text="@this.OSLanguage"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.AccountCircle" Text="@this.OSUserName"/>
|
||||
<MudListItem T="string" Icon="@Icons.Material.Outlined.Business">
|
||||
@switch (HasAnyActiveEnvironment)
|
||||
{
|
||||
@ -278,11 +279,19 @@
|
||||
<ThirdPartyComponent Name="CodeBeam.MudBlazor.Extensions" Developer="Mehmet Can Karagöz & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/CodeBeamOrg/CodeBeam.MudBlazor.Extensions/blob/dev/LICENSE" RepositoryUrl="https://github.com/CodeBeamOrg/CodeBeam.MudBlazor.Extensions" UseCase="@T("This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library.")"/>
|
||||
<ThirdPartyComponent Name="Rust" Developer="Graydon Hoare, Rust Foundation, Rust developers & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/rust/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/rust" UseCase="@T("The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.")"/>
|
||||
<ThirdPartyComponent Name="Tauri" Developer="Daniel Thompson-Yvetot, Lucas Nogueira, Tensor, Boscop, Serge Zaitsev, George Burton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT" RepositoryUrl="https://github.com/tauri-apps/tauri" UseCase="@T("Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!")"/>
|
||||
|
||||
@if (OperatingSystem.IsLinux())
|
||||
{
|
||||
<ThirdPartyComponent Name="GStreamer" Developer="GStreamer contributors & Open Source Community" LicenseName="LGPL-2.1" LicenseUrl="https://gstreamer.freedesktop.org/documentation/frequently-asked-questions/licensing.html" RepositoryUrl="https://gitlab.freedesktop.org/gstreamer/gstreamer" UseCase="@T("Linux AppImages bundle GStreamer components to support microphone access and WebM audio recording in the embedded WebKitGTK web view.")"/>
|
||||
}
|
||||
|
||||
<ThirdPartyComponent Name="Qdrant" Developer="Andrey Vasnetsov, Tim Visée, Arnaud Gourlay, Luis Cossío, Ivan Pleshkov, Roman Titov, xzfc, JojiiOfficial & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/qdrant/qdrant/blob/master/LICENSE" RepositoryUrl="https://github.com/qdrant/qdrant" UseCase="@T("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.")"/>
|
||||
<ThirdPartyComponent Name="Rocket" Developer="Sergio Benitez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rwf2/Rocket/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rwf2/Rocket" UseCase="@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.")"/>
|
||||
<ThirdPartyComponent Name="axum" Developer="David Pedersen, Jonas Platte, tottoto, David Mládek, Yann Simon, Tobias Bieniek, Open Source Community & Tokio Project" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/axum/blob/main/LICENSE" RepositoryUrl="https://github.com/tokio-rs/axum" UseCase="@T("Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running.")"/>
|
||||
<ThirdPartyComponent Name="axum-server" Developer="Eray Karatay, Adi Salimgereyev, daxpedda & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/programatik29/axum-server/blob/master/LICENSE" RepositoryUrl="https://github.com/programatik29/axum-server" UseCase="@T("Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.")"/>
|
||||
<ThirdPartyComponent Name="Rustls" Developer="Joe Birr-Pixton, Dirkjan Ochtman, Daniel McCarney, Brian Smith, Jacob Hoffman-Andrews, Jorge Aparicio & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rustls/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/rustls/rustls" UseCase="@T("Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.")"/>
|
||||
<ThirdPartyComponent Name="serde" Developer="Erick Tryzelaar, David Tolnay & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/serde-rs/serde/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/serde-rs/serde" UseCase="@T("Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.")"/>
|
||||
<ThirdPartyComponent Name="strum_macros" Developer="Peter Glotfelty & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Peternator7/strum/blob/master/LICENSE" RepositoryUrl="https://github.com/Peternator7/strum" UseCase="@T("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.")"/>
|
||||
<ThirdPartyComponent Name="keyring" Developer="Walther Chen, Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/hwchen/keyring-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/hwchen/keyring-rs" UseCase="@T("In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library.")"/>
|
||||
<ThirdPartyComponent Name="keyring-core" Developer="Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/open-source-cooperative/keyring-core/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/open-source-cooperative/keyring-core" UseCase="@T("AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service.")"/>
|
||||
<ThirdPartyComponent Name="arboard" Developer="Artur Kovacs, Avi Weinstock, 1Password & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/arboard/blob/master/LICENSE-MIT.txt" RepositoryUrl="https://github.com/1Password/arboard" UseCase="@T("To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system.")"/>
|
||||
<ThirdPartyComponent Name="tokio" Developer="Alex Crichton, Carl Lerche, Alice Ryhl, Taiki Endo, Ivan Petkov, Eliza Weisman, Lucio Franco & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="@T("Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor.")"/>
|
||||
<ThirdPartyComponent Name="futures" Developer="Alex Crichton, Taiki Endo, Taylor Cramer, Nemo157, Josef Brandl, Aaron Turon & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/futures-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/futures-rs" UseCase="@T("This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow.")"/>
|
||||
@ -299,6 +308,7 @@
|
||||
<ThirdPartyComponent Name="PDFium" Developer="Lei Zhang, Tom Sepez, Dan Sinclair, and Foxit, Google, Chromium, Collabora, Ada, DocsCorp, Dropbox, Microsoft, and PSPDFKit Teams & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE" RepositoryUrl="https://pdfium.googlesource.com/pdfium" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/>
|
||||
<ThirdPartyComponent Name="pdfium-render" Developer="Alastair Carey, Dorian Rudolph & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ajrcarey/pdfium-render/blob/master/LICENSE.md" RepositoryUrl="https://github.com/ajrcarey/pdfium-render" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/>
|
||||
<ThirdPartyComponent Name="sys-locale" Developer="1Password Team, ComplexSpaces & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/sys-locale/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/1Password/sys-locale" UseCase="@T("This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.")"/>
|
||||
<ThirdPartyComponent Name="whoami" Developer="Ardaku Systems, Jeryn Aldaron Lau, Chase Johnson & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ardaku/whoami/blob/stable/LICENSE_MIT" RepositoryUrl="https://github.com/ardaku/whoami" UseCase="@T("This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.")"/>
|
||||
<ThirdPartyComponent Name="sysinfo" Developer="Guillaume Gomez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/GuillaumeGomez/sysinfo/blob/main/LICENSE" RepositoryUrl="https://github.com/GuillaumeGomez/sysinfo" UseCase="@T("This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.")"/>
|
||||
<ThirdPartyComponent Name="tempfile" Developer="Steven Allen, Ashley Mannix & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Stebalien/tempfile/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/Stebalien/tempfile" UseCase="@T("This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.")"/>
|
||||
<ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="@T("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.")" />
|
||||
@ -314,4 +324,4 @@
|
||||
</ExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
</InnerScrolling>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -29,17 +29,18 @@ public partial class Information : MSGComponentBase
|
||||
private ISnackbar Snackbar { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private EmbeddingStore EmbeddingStore { get; init; } = null!;
|
||||
private DatabaseClient DatabaseClient { get; init; } = null!;
|
||||
|
||||
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
|
||||
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!;
|
||||
private static readonly MetaDataArchitectureAttribute META_DATA_ARCH = ASSEMBLY.GetCustomAttribute<MetaDataArchitectureAttribute>()!;
|
||||
private static readonly MetaDataLibrariesAttribute META_DATA_LIBRARIES = ASSEMBLY.GetCustomAttribute<MetaDataLibrariesAttribute>()!;
|
||||
private static readonly MetaDataDatabasesAttribute META_DATA_DATABASES = ASSEMBLY.GetCustomAttribute<MetaDataDatabasesAttribute>()!;
|
||||
private static readonly MetaDataEmbeddingStoreAttribute META_DATA_EMBEDDING_STORE = ASSEMBLY.GetCustomAttribute<MetaDataEmbeddingStoreAttribute>()!;
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Information).Namespace, nameof(Information));
|
||||
|
||||
private string osLanguage = string.Empty;
|
||||
private string osUserName = string.Empty;
|
||||
|
||||
private static string VersionApp => $"MindWork AI Studio: v{META_DATA.Version} (commit {META_DATA.AppCommitHash}, build {META_DATA.BuildNum}, {META_DATA_ARCH.Architecture.ToRID().ToUserFriendlyName()})";
|
||||
|
||||
@ -49,6 +50,8 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private string OSLanguage => $"{T("User-language provided by the OS")}: '{this.osLanguage}'";
|
||||
|
||||
private string OSUserName => $"{T("Username provided by the OS")}: '{this.osUserName}'";
|
||||
|
||||
private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}";
|
||||
|
||||
private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}";
|
||||
@ -59,9 +62,21 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}";
|
||||
|
||||
private string VersionDatabase => this.EmbeddingStore.IsAvailable
|
||||
? $"{T("Database version")}: {this.EmbeddingStore.Name} v{META_DATA_DATABASES.DatabaseVersion}"
|
||||
: $"{T("Database")}: {this.EmbeddingStore.Name} - {T("not available")}";
|
||||
private string VersionEmbeddingStore
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.embeddingStore is null)
|
||||
return $"{T("Embedding store")}: {T("checking availability")}";
|
||||
|
||||
return this.embeddingStore.Status switch
|
||||
{
|
||||
EmbeddingStoreStatus.AVAILABLE => $"{T("Embedding store version")}: {this.embeddingStore.Name} v{META_DATA_EMBEDDING_STORE.DatabaseVersion}",
|
||||
EmbeddingStoreStatus.STARTING => $"{T("Embedding store")}: {this.embeddingStore.Name} - {T("starting")}",
|
||||
_ => $"{T("Embedding store")}: {this.embeddingStore.Name} - {T("not available")}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private string versionPandoc = TB("Determine Pandoc version, please wait...");
|
||||
private PandocInstallation pandocInstallation;
|
||||
@ -70,7 +85,7 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private bool showEnterpriseConfigDetails;
|
||||
|
||||
private bool showDatabaseDetails;
|
||||
private bool showEmbeddingStoreDetails;
|
||||
|
||||
private List<IAvailablePlugin> configPlugins = PluginFactory.AvailablePlugins
|
||||
.Where(x => x.Type is PluginType.CONFIGURATION)
|
||||
@ -81,11 +96,13 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private List<MandatoryInfoPanelData> mandatoryInfoPanels = [];
|
||||
|
||||
private sealed record DatabaseDisplayInfo(string Label, string Value);
|
||||
private sealed record EmbeddingStoreDisplayInfo(string Label, string Value);
|
||||
|
||||
private sealed record MandatoryInfoPanelData(string HeaderText, string PluginName, DataMandatoryInfo Info, DataMandatoryInfoAcceptance? Acceptance);
|
||||
|
||||
private readonly List<DatabaseDisplayInfo> databaseDisplayInfo = new();
|
||||
private readonly List<EmbeddingStoreDisplayInfo> embeddingStoreDisplayInfo = new();
|
||||
private DatabaseClient? embeddingStore;
|
||||
private CancellationTokenSource? databaseRefreshCancellationTokenSource;
|
||||
|
||||
private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive);
|
||||
|
||||
@ -128,12 +145,12 @@ public partial class Information : MSGComponentBase
|
||||
this.RefreshEnterpriseConfigurationState();
|
||||
|
||||
this.osLanguage = await this.RustService.ReadUserLanguage();
|
||||
this.osUserName = await this.RustService.ReadUserName();
|
||||
this.logPaths = await this.RustService.GetLogPaths();
|
||||
|
||||
await foreach (var (label, value) in this.EmbeddingStore.GetDisplayInfo())
|
||||
{
|
||||
this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value));
|
||||
}
|
||||
await this.RefreshDatabaseInfo(CancellationToken.None);
|
||||
if (this.databaseClient?.Status is DatabaseClientStatus.STARTING)
|
||||
this.StartShortDatabaseRefreshLoop();
|
||||
|
||||
// Determine the Pandoc version may take some time, so we start it here
|
||||
// without waiting for the result:
|
||||
@ -234,7 +251,70 @@ public partial class Information : MSGComponentBase
|
||||
|
||||
private void ToggleDatabaseDetails()
|
||||
{
|
||||
this.showDatabaseDetails = !this.showDatabaseDetails;
|
||||
this.showEmbeddingStoreDetails = !this.showEmbeddingStoreDetails;
|
||||
}
|
||||
|
||||
private async Task RefreshDatabaseInfo(CancellationToken cancellationToken)
|
||||
{
|
||||
var refreshedClient = await this.DatabaseClientProvider.RefreshClientAsync(DatabaseRole.VECTOR_STORE, cancellationToken);
|
||||
this.databaseClient = refreshedClient;
|
||||
this.embeddingStoreDisplayInfo.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var (label, value) in refreshedClient.GetDisplayInfo().WithCancellation(cancellationToken))
|
||||
{
|
||||
this.embeddingStoreDisplayInfo.Add(new EmbeddingStoreDisplayInfo(label, value));
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.databaseClient = new NoDatabaseClient(refreshedClient.Name, e.Message, DatabaseClientStatus.STARTING);
|
||||
await foreach (var (label, value) in this.databaseClient.GetDisplayInfo().WithCancellation(cancellationToken))
|
||||
{
|
||||
this.embeddingStoreDisplayInfo.Add(new EmbeddingStoreDisplayInfo(label, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartShortDatabaseRefreshLoop()
|
||||
{
|
||||
this.databaseRefreshCancellationTokenSource?.Cancel();
|
||||
this.databaseRefreshCancellationTokenSource?.Dispose();
|
||||
this.databaseRefreshCancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = this.databaseRefreshCancellationTokenSource.Token;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
const int MAX_TRIES = 12;
|
||||
for (var attempt = 0; attempt < MAX_TRIES; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
await this.InvokeAsync(async () =>
|
||||
{
|
||||
await this.RefreshDatabaseInfo(cancellationToken);
|
||||
this.StateHasChanged();
|
||||
});
|
||||
|
||||
if (this.databaseClient?.Status is not DatabaseClientStatus.STARTING)
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId)
|
||||
@ -249,6 +329,13 @@ public partial class Information : MSGComponentBase
|
||||
return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId;
|
||||
}
|
||||
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
this.databaseRefreshCancellationTokenSource?.Cancel();
|
||||
this.databaseRefreshCancellationTokenSource?.Dispose();
|
||||
base.DisposeResources();
|
||||
}
|
||||
|
||||
private async Task CopyStartupLogPath()
|
||||
{
|
||||
await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath);
|
||||
|
||||
@ -10,6 +10,7 @@ namespace AIStudio.Pages;
|
||||
|
||||
public partial class Writer : MSGComponentBase
|
||||
{
|
||||
private static readonly ILogger<Writer> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Writer>();
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500));
|
||||
|
||||
@ -106,22 +107,38 @@ public partial class Writer : MSGComponentBase
|
||||
InitialRemoteWait = true,
|
||||
};
|
||||
|
||||
this.chatThread?.Blocks.Add(new ContentBlock
|
||||
var aiBlock = new ContentBlock
|
||||
{
|
||||
Time = time,
|
||||
ContentType = ContentType.TEXT,
|
||||
Role = ChatRole.AI,
|
||||
Content = aiText,
|
||||
});
|
||||
};
|
||||
|
||||
this.chatThread?.Blocks.Add(aiBlock);
|
||||
|
||||
this.isStreaming = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.providerSettings.Model, lastUserPrompt, this.chatThread);
|
||||
this.suggestion = aiText.Text;
|
||||
|
||||
this.isStreaming = false;
|
||||
this.StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.providerSettings.Model, lastUserPrompt, this.chatThread);
|
||||
this.suggestion = aiText.Text;
|
||||
}
|
||||
catch (ProviderRequestException e)
|
||||
{
|
||||
LOGGER.LogError(e, "The provider request failed for writer suggestions. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", e.StatusCode, e.ReasonPhrase, e.ResponseBody);
|
||||
await this.MessageBus.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
|
||||
this.suggestion = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(aiText.Text))
|
||||
this.chatThread?.Blocks.Remove(aiBlock);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isStreaming = false;
|
||||
this.StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void AcceptEntireSuggestion()
|
||||
|
||||
@ -142,6 +142,54 @@ CONFIG["EMBEDDING_PROVIDERS"] = {}
|
||||
-- }
|
||||
-- }
|
||||
|
||||
-- ERI v1 data sources for retrieval-augmented generation:
|
||||
CONFIG["DATA_SOURCES"] = {}
|
||||
|
||||
-- Example: ERI v1 data source with a shared access token.
|
||||
-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
|
||||
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
|
||||
-- ["Name"] = "<user-friendly data source name>",
|
||||
-- ["Type"] = "ERI_V1",
|
||||
-- ["Hostname"] = "<https address of the ERI server>",
|
||||
-- ["Port"] = 443,
|
||||
-- ["AuthMethod"] = "TOKEN",
|
||||
-- ["Token"] = "ENC:v1:<base64-encoded encrypted token>",
|
||||
-- ["SecurityPolicy"] = "SELF_HOSTED",
|
||||
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
|
||||
-- ["MaxMatches"] = 10,
|
||||
-- }
|
||||
|
||||
-- Example: ERI v1 data source with a shared username and password.
|
||||
-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
|
||||
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
|
||||
-- ["Name"] = "<user-friendly data source name>",
|
||||
-- ["Type"] = "ERI_V1",
|
||||
-- ["Hostname"] = "<https address of the ERI server>",
|
||||
-- ["Port"] = 443,
|
||||
-- ["AuthMethod"] = "USERNAME_PASSWORD",
|
||||
-- ["UsernamePasswordMode"] = "SHARED_USERNAME_AND_PASSWORD",
|
||||
-- ["Username"] = "<shared username>",
|
||||
-- ["Password"] = "ENC:v1:<base64-encoded encrypted password>",
|
||||
-- ["SecurityPolicy"] = "SELF_HOSTED",
|
||||
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
|
||||
-- ["MaxMatches"] = 10,
|
||||
-- }
|
||||
|
||||
-- Example: ERI v1 data source using the user's username and a shared password.
|
||||
-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
|
||||
-- ["Id"] = "00000000-0000-0000-0000-000000000000",
|
||||
-- ["Name"] = "<user-friendly data source name>",
|
||||
-- ["Type"] = "ERI_V1",
|
||||
-- ["Hostname"] = "<https address of the ERI server>",
|
||||
-- ["Port"] = 443,
|
||||
-- ["AuthMethod"] = "USERNAME_PASSWORD",
|
||||
-- ["UsernamePasswordMode"] = "OS_USERNAME_SHARED_PASSWORD",
|
||||
-- ["Password"] = "ENC:v1:<base64-encoded encrypted password>",
|
||||
-- ["SecurityPolicy"] = "SELF_HOSTED",
|
||||
-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>",
|
||||
-- ["MaxMatches"] = 10,
|
||||
-- }
|
||||
|
||||
CONFIG["SETTINGS"] = {}
|
||||
|
||||
-- Configure the update check interval:
|
||||
@ -178,9 +226,9 @@ CONFIG["SETTINGS"] = {}
|
||||
-- CONFIG["SETTINGS"]["DataApp.PreviewVisibility"] = "NONE"
|
||||
|
||||
-- Configure the enabled preview features:
|
||||
-- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs
|
||||
-- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_SPEECH_TO_TEXT_2026.
|
||||
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_SPEECH_TO_TEXT_2026" }
|
||||
-- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/blob/main/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs
|
||||
-- Examples are PRE_WRITER_MODE_2024 and PRE_RAG_2024.
|
||||
-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024" }
|
||||
|
||||
-- Configure the preselected provider.
|
||||
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
|
||||
@ -218,6 +266,10 @@ CONFIG["SETTINGS"] = {}
|
||||
-- Examples are: "CmdOrControl+Shift+D", "Alt+F9", "F8"
|
||||
-- CONFIG["SETTINGS"]["DataApp.ShortcutVoiceRecording"] = "CmdOrControl+1"
|
||||
|
||||
-- Configure the HTTP timeout for external requests, in seconds.
|
||||
-- The default is 3600 (1 hour).
|
||||
-- CONFIG["SETTINGS"]["DataApp.HttpClientTimeoutSeconds"] = 3600
|
||||
|
||||
-- Example chat templates for this configuration:
|
||||
CONFIG["CHAT_TEMPLATES"] = {}
|
||||
|
||||
@ -252,7 +304,8 @@ CONFIG["CHAT_TEMPLATES"] = {}
|
||||
-- ["AllowProfileUsage"] = true,
|
||||
-- -- Optional: Pre-attach files that will be automatically included when using this template.
|
||||
-- -- These files will be loaded when the user selects this chat template.
|
||||
-- -- Note: File paths must be absolute paths and accessible to all users.
|
||||
-- -- Note: File paths can be absolute paths that are accessible to all users, or relative paths
|
||||
-- -- inside this plugin folder, for example "attachments/00000000-0000-0000-0000-000000000001/Guidelines.pdf".
|
||||
-- ["FileAttachments"] = {
|
||||
-- "G:\\Company\\Documents\\Guidelines.pdf",
|
||||
-- "G:\\Company\\Documents\\CompanyPolicies.docx"
|
||||
|
||||
@ -2649,6 +2649,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1599198973"]
|
||||
-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Möchten Sie eines ihrer Profile als Standard für die gesamte App festlegen? Wenn Sie einem Assistenten ein anderes Profil zuweisen, hat dieses immer Vorrang."
|
||||
|
||||
-- seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "Sekunden"
|
||||
|
||||
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Wählen Sie für die Transkription Ihrer Stimme einen Anbieter für Transkriptionen aus. Ohne einen ausgewählten Anbieter wird die Diktier- und Transkriptions-Funktion deaktiviert."
|
||||
|
||||
@ -2697,6 +2700,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"]
|
||||
-- Spellchecking is enabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Rechtschreibprüfung ist aktiviert"
|
||||
|
||||
-- Request timeout
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3569531009"] = "Zeitüberschreitung bei der Anfrage"
|
||||
|
||||
-- App Options
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App-Einstellungen"
|
||||
|
||||
@ -2724,6 +2730,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"]
|
||||
-- Select a transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Wählen Sie einen Transkriptionsanbieter aus"
|
||||
|
||||
-- How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] = "Wie lange AI Studio auf externe HTTP-Anfragen wartet, z. B. an KI-Anbieter, Einbettungen, Transkription, ERI-Datenquellen und Downloads von Enterprise-Konfigurationen."
|
||||
|
||||
-- Navigation bar behavior
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Verhalten der Navigationsleiste"
|
||||
|
||||
@ -3123,6 +3132,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Beginnen
|
||||
-- Transcription in progress...
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2851219233"] = "Transkription läuft …"
|
||||
|
||||
-- Unfortunately, there was an error communicating with the AI system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T3236134591"] = "Leider ist bei der Kommunikation mit dem KI-System ein Fehler aufgetreten."
|
||||
|
||||
-- The configured transcription provider was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T331613105"] = "Der konfigurierte Anbieter für die Transkription wurde nicht gefunden."
|
||||
|
||||
@ -3636,6 +3648,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2879113658"] =
|
||||
-- Maximum matches per query
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximale Treffer pro Abfrage"
|
||||
|
||||
-- Failed to read the user's username from the operating system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2909734556"] = "Der Benutzername des Nutzers konnte nicht aus dem Betriebssystem gelesen werden."
|
||||
|
||||
-- Open web link, show more information
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Weblink öffnen & mehr Informationen anzeigen"
|
||||
|
||||
@ -3687,6 +3702,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T742006305"] = "
|
||||
-- Embeddings
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Einbettungen"
|
||||
|
||||
-- Use the same username and password for all users
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1769874785"] = "Für alle Benutzer denselben Benutzernamen und dasselbe Passwort verwenden"
|
||||
|
||||
-- Username and password mode
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1787063064"] = "Modus für den Benutzernamen und das Passwort"
|
||||
|
||||
-- How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3081234668"] = "Wie soll AI Studio die Konfiguration von Benutzername und Passwort für die ERI-v1-Datenquelle „{0}“ exportieren?"
|
||||
|
||||
-- User-managed username and password
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T365340972"] = "Vom Benutzer verwaltete Anmeldedaten (Benutzername und Passwort)"
|
||||
|
||||
-- Export
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3898821075"] = "Exportieren"
|
||||
|
||||
-- Read each user's username from the operating system and share one password
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T76405695"] = "Den Benutzernamen jedes Benutzers aus dem Betriebssystem auslesen und ein Passwort teilen."
|
||||
|
||||
-- Cancel
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T900713019"] = "Abbrechen"
|
||||
|
||||
-- Describe what data this directory contains to help the AI select it.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Beschreiben Sie, welche Daten dieses Verzeichnis enthält, um der KI bei der Auswahl zu helfen."
|
||||
|
||||
@ -4722,6 +4758,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T582516016"] =
|
||||
-- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Passen Sie ihre KI-Erfahrung mit Chat-Vorlagen an. Egal, ob Sie mit Prompt-Engineering experimentieren, einfach einen eigenen System-Prompt im normalen Chat verwenden oder einen spezialisierten Assistenten erstellen möchten – mit Chat-Vorlagen haben Sie die volle Kontrolle. Ähnlich wie in den Playgrounds gängiger KI-Anbieter können Sie eigene System-Prompts festlegen und bei unterstützenden Anbietern auch Assistenten-Prompts nutzen."
|
||||
|
||||
-- Copy attachments into plugin
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1345613295"] = "Anhänge in das Plugin kopieren"
|
||||
|
||||
-- Delete
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Löschen"
|
||||
|
||||
@ -4731,6 +4770,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T15483
|
||||
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Hinweis: Diese fortgeschrittene Funktion richtet sich an Nutzer, die mit den Grundlagen des Prompt Engineerings vertraut sind. Außerdem müssen Sie selbst sicherstellen, dass Ihr gewählter Anbieter die Verwendung von Assistenten-Prompts unterstützt."
|
||||
|
||||
-- Use shared attachment paths
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2054531878"] = "Gemeinsame Pfade für Anhänge verwenden"
|
||||
|
||||
-- No chat templates configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "Noch keine Chat-Vorlagen konfiguriert."
|
||||
|
||||
@ -4749,6 +4791,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T34481
|
||||
-- This template is managed by your organization.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "Diesee Vorlage wird von Ihrer Organisation verwaltet."
|
||||
|
||||
-- Select configuration plugin folder
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576816894"] = "Konfigurationsordner für Plugins auswählen"
|
||||
|
||||
-- Edit Chat Template
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Chat-Vorlage bearbeiten"
|
||||
|
||||
@ -4761,6 +4806,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38650
|
||||
-- Delete Chat Template
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Chat-Vorlage löschen"
|
||||
|
||||
-- Export Chat Template
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Chat-Vorlage exportieren"
|
||||
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T975426229"] = "Konfiguration exportieren"
|
||||
|
||||
-- Which programming language should be preselected for added contexts?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Welche Programmiersprache soll für hinzugefügte Kontexte vorausgewählt werden?"
|
||||
|
||||
@ -4815,6 +4866,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T145419
|
||||
-- Delete
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Löschen"
|
||||
|
||||
-- Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1577531115"] = "Kerberos-/SSO-ERI-Datenquellen können noch nicht exportiert werden. Bitte konfigurieren Sie diese manuell im Konfigurations-Plugin."
|
||||
|
||||
-- Cannot export this ERI data source because the authentication secret could not be encrypted.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1592527757"] = "Diese ERI-Datenquelle kann nicht exportiert werden, da das Authentifizierungsgeheimnis nicht verschlüsselt werden konnte."
|
||||
|
||||
-- External (ERI)
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "Extern (ERI)"
|
||||
|
||||
@ -4845,6 +4902,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T269820
|
||||
-- Embedding
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Einbettung"
|
||||
|
||||
-- This data source is managed by your organization.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3031462878"] = "Diese Datenquelle wird von Ihrer Organisation verwaltet."
|
||||
|
||||
-- Edit
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Bearbeiten"
|
||||
|
||||
@ -4869,21 +4929,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T352566
|
||||
-- No data sources configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "Noch keine Datenquellen konfiguriert."
|
||||
|
||||
-- Export Access Token?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3595669127"] = "Zugriffstoken exportieren?"
|
||||
|
||||
-- Export ERI Data Source
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3831281036"] = "ERI-Datenquelle exportieren"
|
||||
|
||||
-- Actions
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Aktionen"
|
||||
|
||||
-- This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T4027572258"] = "Für diese ERI-Datenquelle ist ein Zugriffstoken konfiguriert. Möchten Sie das verschlüsselte Zugriffstoken in den Export aufnehmen? Hinweis: Der Empfänger benötigt dasselbe Geheimnis für die Verschlüsselung, um das Zugriffstoken verwenden zu können."
|
||||
|
||||
-- Configured Data Sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Konfigurierte Datenquellen"
|
||||
|
||||
-- Add ERI v1 Data Source
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "ERI v1 Datenquelle hinzufügen"
|
||||
|
||||
-- Cannot export this ERI data source because no enterprise encryption secret is configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T750361472"] = "Diese ERI-Datenquelle kann nicht exportiert werden, da kein Geheimnis für die Verschlüsselung konfiguriert ist."
|
||||
|
||||
-- External Data (ERI-Server v1)
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "Externe Daten (ERI-Server v1)"
|
||||
|
||||
-- Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T782820095"] = "Diese ERI-Datenquelle kann nicht exportiert werden, da kein Authentifizierungsgeheimnis konfiguriert ist. Das Problem war: {0}"
|
||||
|
||||
-- Local Directory
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Lokaler Ordner"
|
||||
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T975426229"] = "Konfiguration exportieren"
|
||||
|
||||
-- When enabled, you can preselect some ERI server options.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "Wenn aktiviert, können Sie einige ERI-Serveroptionen vorauswählen."
|
||||
|
||||
@ -5169,6 +5247,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T55364659"
|
||||
-- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Sind Sie Projektleiter in einer Forschungseinrichtung? Dann möchten Sie vielleicht ein Profil für ihre Projektmanagement-Aktivitäten anlegen, eines für ihre wissenschaftliche Arbeit und ein weiteres Profil, wenn Sie Programmcode schreiben müssen. In diesen Profilen können Sie festhalten, wie viel Erfahrung Sie haben oder welche Methoden Sie bevorzugen oder nicht gerne verwenden. Später können Sie dann auswählen, wann und wo Sie jedes Profil nutzen möchten."
|
||||
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T975426229"] = "Konfiguration exportieren"
|
||||
|
||||
-- Preselect the target language
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Zielsprache vorwählen"
|
||||
|
||||
@ -6027,18 +6108,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1890416390"] = "Nach Updates suc
|
||||
-- Vision
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision"
|
||||
|
||||
-- In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "Um ein beliebiges LLM nutzen zu können, muss jeder User seinen sogenannten API-Schlüssel für jeden LLM-Anbieter speichern. Dieser Schlüssel muss sicher aufbewahrt werden – ähnlich wie ein Passwort. Die sicherste Methode hierfür bieten Betriebssysteme wie macOS, Windows und Linux: Sie verfügen über Mechanismen, solche Daten – sofern vorhanden – auf spezieller Sicherheits-Hardware zu speichern. Da dies derzeit in .NET nicht möglich ist, verwenden wir diese Rust-Bibliothek."
|
||||
|
||||
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek wird verwendet, um HTML in Markdown umzuwandeln. Das ist zum Beispiel notwendig, wenn Sie eine URL als Eingabe für einen Assistenten angeben."
|
||||
|
||||
-- Encryption secret: is configured
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Geheimnis für die Verschlüsselung: ist konfiguriert"
|
||||
|
||||
-- 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."
|
||||
|
||||
-- Copies the following to the clipboard
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgendes in die Zwischenablage"
|
||||
|
||||
@ -6066,6 +6141,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Konfigurations-P
|
||||
-- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2329884315"] = "Die Programmiersprache C# wird für die Umsetzung der Benutzeroberfläche und des Backends verwendet. Für die Entwicklung der Benutzeroberfläche mit C# kommt die Blazor-Technologie aus ASP.NET Core zum Einsatz. Alle diese Technologien sind im .NET SDK integriert."
|
||||
|
||||
-- Linux AppImages bundle GStreamer components to support microphone access and WebM audio recording in the embedded WebKitGTK web view.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T234598990"] = "Linux-AppImages bündeln GStreamer-Komponenten, um den Mikrofonzugriff und WebM-Audioaufnahmen in der eingebetteten WebKitGTK-Webansicht zu unterstützen."
|
||||
|
||||
-- Used PDFium version
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2368247719"] = "Verwendete PDFium-Version"
|
||||
|
||||
@ -6120,6 +6198,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Verwendete .NET-
|
||||
-- Explanation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Erklärung"
|
||||
|
||||
-- checking availability
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "Verfügbarkeit wird geprüft"
|
||||
|
||||
-- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend kann nicht als Desktop-App gestartet werden. Deshalb verwende ich ein zweites Backend in Rust, das ich „Runtime“ nenne. Mit Rust als Runtime kann Tauri genutzt werden, um eine typische Desktop-App zu realisieren. Dank Rust kann diese App für Windows-, macOS- und Linux-Desktops angeboten werden. Rust ist eine großartige Sprache für die Entwicklung sicherer und leistungsstarker Software."
|
||||
|
||||
@ -6141,6 +6222,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Haben Sie Ideen
|
||||
-- Hide Details
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Details ausblenden"
|
||||
|
||||
-- Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "Der Axum-Server führt den internen Axum-Dienst über eine sichere lokale Verbindung aus. Dadurch kann AI Studio die Kommunikation zwischen der Rust-Laufzeitumgebung und der Benutzeroberfläche schützen."
|
||||
|
||||
-- Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3239817808"] = "Rustls hilft dabei, die interne Verbindung zwischen der Benutzeroberfläche der App und der Rust-Laufzeitumgebung abzusichern. Dadurch wird die lokale Kommunikation geschützt, die AI Studio während der Ausführung benötigt."
|
||||
|
||||
-- Update Pandoc
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Pandoc aktualisieren"
|
||||
|
||||
@ -6165,6 +6252,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio wird m
|
||||
-- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwendet, um die Blazor-Benutzeroberfläche bereitzustellen. Es ist ein großartiges Projekt, das die Erstellung von Desktop-Anwendungen mit Webtechnologien ermöglicht. Ich liebe Tauri!"
|
||||
|
||||
-- AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "AI Studio speichert vertrauliche Daten wie API-Schlüssel im sicheren Speicher Ihres Betriebssystems. Die Bibliothek keyring-core übernimmt dies, indem sie eine Verbindung zum macOS-Schlüsselbund, zur Windows-Anmeldeinformationsverwaltung und zum Linux Secret Service herstellt."
|
||||
|
||||
-- Motivation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
|
||||
|
||||
@ -6174,6 +6264,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "nicht verfügbar
|
||||
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen."
|
||||
|
||||
-- Username provided by the OS
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Vom Betriebssystem bereitgestellter Benutzername"
|
||||
|
||||
-- this version does not met the requirements
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht"
|
||||
|
||||
@ -6195,6 +6288,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen"
|
||||
-- Database
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Datenbank"
|
||||
|
||||
-- This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "Diese Bibliothek wird von der Rust-Laufzeitumgebung verwendet, um den Benutzernamen des aktuellen Benutzers auszulesen, z. B. wenn ein von einer Organisation verwalteter ERI-Server den OS-Benutzernamen für die Authentifizierung verwendet."
|
||||
|
||||
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "Diese Bibliothek wird verwendet, um asynchrone Datenströme in Rust zu erstellen. Sie ermöglicht es uns, mit Datenströmen zu arbeiten, die asynchron bereitgestellt werden, wodurch sich Ereignisse oder Daten, die nach und nach eintreffen, leichter verarbeiten lassen. Wir nutzen dies zum Beispiel, um beliebige Daten aus dem Dateisystem an das Einbettungssystem zu übertragen."
|
||||
|
||||
@ -6215,6 +6311,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Verwendetes .NET
|
||||
|
||||
-- We use the DeepSeek Tokenizer to estimate the number of tokens an input will generate.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T591393704"] = "Wir verwenden den DeepSeek‑Tokenizer, um die Token‑Anzahl einer Eingabe zu schätzen."
|
||||
-- starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "wird gestartet"
|
||||
|
||||
-- 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."
|
||||
@ -6237,6 +6335,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Bereitgestellt vo
|
||||
-- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "Wir verwenden diese Bibliothek, um PowerPoint-Dateien lesen zu können. So ist es möglich, Inhalte aus Folien in Prompts einzufügen und PowerPoint-Dateien in RAG-Prozessen zu berücksichtigen. Wir danken Nils Kruthoff für seine Arbeit an diesem Rust-Crate."
|
||||
|
||||
-- Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "Axum wird verwendet, um den kleinen internen Dienst bereitzustellen, der die Rust-Laufzeitumgebung mit der Benutzeroberfläche der App verbindet. So können beide Teile von AI Studio Informationen austauschen, während die App läuft."
|
||||
|
||||
-- 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."
|
||||
|
||||
@ -6378,6 +6479,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Ihre Regieanweisungen"
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Der Server ist möglicherweise nicht erreichbar oder hat Probleme. Die Nachricht des Anbieters lautet: „{2}“"
|
||||
|
||||
-- The provider '{0}' reported an error while streaming the response.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1008706234"] = "Der Anbieter „{0}“ hat einen Fehler beim Streamen der Antwort gemeldet."
|
||||
|
||||
-- The provider rejected the request because too many requests were sent. Please wait a moment and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1028424693"] = "Der Anbieter hat die Anfrage abgelehnt, weil zu viele Anfragen gesendet wurden. Bitte warten Sie einen Moment und versuchen Sie es erneut."
|
||||
|
||||
-- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "Die Anfrage an den LLM-Anbieter „{0}“ (Typ={1}) hat nach {2} während „{3}“ das Zeitlimit überschritten. Bitte versuchen Sie es erneut oder prüfen Sie, ob der Anbieter noch antwortet."
|
||||
|
||||
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Beim Versuch, die Antwort des LLM-Anbieters '{0}' zu streamen, sind Probleme aufgetreten. Die Meldung lautet: '{1}'"
|
||||
|
||||
@ -6408,6 +6518,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3759732886"] = "Wir haben ve
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T4049517041"] = "Wir haben versucht, mit dem LLM-Anbieter „{0}“ (Typ={1}) zu kommunizieren. Die Daten des Chats, einschließlich aller Dateianhänge, sind vermutlich zu groß für das ausgewählte Modell und den Anbieter. Die Nachricht des Anbieters lautet: „{2}“"
|
||||
|
||||
-- The provider '{0}' reported an error: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T700894460"] = "Der Anbieter „{0}“ hat einen Fehler gemeldet: {1}"
|
||||
|
||||
-- The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "Das Vertrauensniveau dieses Anbieters wurde **noch nicht** gründlich **untersucht und bewertet**. Wir wissen nicht, ob ihre Daten sicher sind."
|
||||
|
||||
@ -6468,6 +6581,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "Kein Modell ausgew
|
||||
-- We could not load models from '{0}'. The account or API key does not have the required permissions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T1143085203"] = "Wir konnten keine Modelle von '{0}' laden. Das Konto oder der API-Schlüssel verfügt nicht über die erforderlichen Berechtigungen."
|
||||
|
||||
-- We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T155481725"] = "Wir konnten keine Modelle von „{0}“ laden, da zu viele Anfragen gesendet wurden. Bitte warten Sie einen Moment und versuchen Sie es erneut."
|
||||
|
||||
-- We could not load models from '{0}'. The API key is probably missing, invalid, or expired.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2041046579"] = "Modelle aus '{0}' konnten nicht geladen werden. Wahrscheinlich fehlt der API-Schlüssel, ist ungültig oder abgelaufen."
|
||||
|
||||
@ -6477,15 +6593,39 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T21156887
|
||||
-- We could not load models from '{0}' because the provider returned an unexpected response.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2186844789"] = "Wir konnten keine Modelle von '{0}' laden, da der Anbieter eine unerwartete Antwort zurückgegeben hat."
|
||||
|
||||
-- We could not load models from '{0}' because the account appears to have no API credits left.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T373339048"] = "Modelle konnten nicht von „{0}“ geladen werden, da das Konto offenbar keine API-Guthaben mehr hat."
|
||||
|
||||
-- We could not load models from '{0}' due to an unknown error.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "Wir konnten die Modelle aus '{0}' aufgrund eines unbekannten Fehlers nicht laden."
|
||||
|
||||
-- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "Anscheinend haben Sie bei OpenAI kein API-Guthaben mehr. Bitte fügen Sie Ihrem Konto Guthaben hinzu und versuchen Sie es erneut."
|
||||
|
||||
-- Model as configured by whisper.cpp
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Modell wie in whisper.cpp konfiguriert"
|
||||
|
||||
-- Cannot export this chat template because example message {0} is not a text message.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Diese Chatvorlage kann nicht exportiert werden, da die Beispielnachricht {0} keine Textnachricht ist."
|
||||
|
||||
-- Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2407395493"] = "Diese Chat-Vorlage kann nicht exportiert werden, da die Beispielnachricht {0} eine Rolle verwendet, die von Konfigurations-Plugins nicht unterstützt wird."
|
||||
|
||||
-- Please select a valid configuration plugin folder. The folder must contain a plugin.lua file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2542895569"] = "Bitte wählen Sie einen gültigen Konfigurations-Plug-in-Ordner aus. Der Ordner muss eine Datei „plugin.lua“ enthalten."
|
||||
|
||||
-- Cannot package the chat template attachments. The issue was: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T3635593138"] = "Die Anhänge der Chat-Vorlage können nicht verpackt werden. Das Problem war: {0}"
|
||||
|
||||
-- Cannot package the attachment '{0}' because the file does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4121340492"] = "Der Anhang „{0}“ kann nicht gepackt werden, da die Datei nicht existiert."
|
||||
|
||||
-- Use no chat template
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Keine Chat-Vorlage verwenden"
|
||||
|
||||
-- Cannot export this chat template because example message {0} is empty.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T477540958"] = "Diese Chatvorlage kann nicht exportiert werden, da die Beispielnachricht {0} leer ist."
|
||||
|
||||
-- Navigation never expands, but there are tooltips
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Die Navigationsleiste wird nie ausgeklappt, aber es gibt Tooltips"
|
||||
|
||||
@ -6681,8 +6821,8 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2708
|
||||
-- Unknown preview feature
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unbekannte Vorschau-Funktion"
|
||||
|
||||
-- Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T714355911"] = "Transkription: Vorschau unseres Sprache-zu-Text-Systems, mit dem Sie Aufnahmen und Audiodateien in Text transkribieren können"
|
||||
-- Transcription: Convert recordings and audio files into text
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T4247148645"] = "Transkription: Aufnahmen und Audiodateien in Text umwandeln"
|
||||
|
||||
-- Use no data sources, when sending an assistant result to a chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Keine Datenquellen vorauswählen, wenn ein Ergebnis von einem Assistenten an einen neuen Chat gesendet wird"
|
||||
@ -6708,6 +6848,21 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::THEMESEXTENSIONS::T534715610"] =
|
||||
-- Use no profile
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::PROFILE::T2205839602"] = "Kein Profil verwenden"
|
||||
|
||||
-- The selected model is not available.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T1578005752"] = "Das ausgewählte Modell ist nicht verfügbar."
|
||||
|
||||
-- The selected provider is not allowed for this chat.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T174545104"] = "Der ausgewählte Anbieter ist für diesen Chat nicht zulässig."
|
||||
|
||||
-- The AI job failed. The message is: '{0}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T237448388"] = "Der KI-Auftrag ist fehlgeschlagen. Die Meldung lautet: „{0}“"
|
||||
|
||||
-- The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T3267850764"] = "Das ausgewählte Modell „{0}“ ist bei „{1}“ nicht mehr verfügbar (Anbieter={2}). Bitte passen Sie Ihre Anbietereinstellungen an."
|
||||
|
||||
-- We could load models from '{0}', but the provider did not return any usable text models.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T3378120620"] = "Wir konnten Modelle von „{0}“ laden, aber der Anbieter hat keine verwendbaren Textmodelle zurückgegeben."
|
||||
|
||||
-- SSO (Kerberos)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AUTHMETHODSV1EXTENSIONS::T268552140"] = "SSO (Kerberos)"
|
||||
|
||||
@ -6849,6 +7004,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "
|
||||
-- Reason
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Grund"
|
||||
|
||||
-- Starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Wird gestartet"
|
||||
|
||||
-- Unavailable
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Nicht verfügbar"
|
||||
|
||||
@ -6933,6 +7091,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2858189239"] = "Authe
|
||||
-- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Die Sicherheitsanforderungen konnten nicht abgerufen werden: Die Anfrage wurde entweder vom Benutzer abgebrochen oder ist aufgrund eines Zeitüberschreitungsfehlers fehlgeschlagen."
|
||||
|
||||
-- Failed to read the user's username from the operating system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2909734556"] = "Der Benutzername konnte nicht aus dem Betriebssystem ausgelesen werden."
|
||||
|
||||
-- Failed to retrieve the security requirements due to an exception: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Die Sicherheitsanforderungen konnten wegen eines Problems nicht abgerufen werden: {0}"
|
||||
|
||||
@ -6978,6 +7139,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Fehler
|
||||
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Fehler beim Abrufen der Authentifizierungsmethoden: Der ERI-Server hat keine gültige Antwort zurückgegeben."
|
||||
|
||||
-- AI Studio couldn't install Pandoc because the archive was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio konnte Pandoc nicht installieren, da das Archiv nicht gefunden wurde."
|
||||
|
||||
-- Pandoc doesn't seem to be installed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc scheint nicht installiert zu sein."
|
||||
|
||||
-- Was not able to validate the Pandoc installation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Die Pandoc-Installation konnte nicht überprüft werden."
|
||||
|
||||
@ -6999,20 +7166,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} ist insta
|
||||
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} ist installiert, entspricht aber nicht der benötigten Version (v{1})."
|
||||
|
||||
-- Pandoc was not installed successfully, because the archive was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc wurde nicht erfolgreich installiert, da das Archiv nicht gefunden wurde."
|
||||
-- AI Studio couldn't install Pandoc because the archive type is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio konnte Pandoc nicht installieren, da der Archivtyp unbekannt ist."
|
||||
|
||||
-- Pandoc is not available on the system or the process had issues.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc ist auf dem System nicht verfügbar oder der Vorgang ist auf Probleme gestoßen."
|
||||
|
||||
-- Pandoc was not installed successfully, because the archive type is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc wurde nicht erfolgreich installiert, da der Archivtyp unbekannt ist."
|
||||
-- AI Studio couldn't install Pandoc because the executable was not found in the archive.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio konnte Pandoc nicht installieren, da die ausführbare Datei im Archiv nicht gefunden wurde."
|
||||
|
||||
-- It seems that Pandoc is not installed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "Es scheint, dass Pandoc nicht installiert ist."
|
||||
-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio konnte die neueste Pandoc-Version nicht finden und installiert stattdessen Version {0}."
|
||||
|
||||
-- The latest Pandoc version was not found, installing version {0} instead.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "Die neueste Pandoc-Version wurde nicht gefunden, stattdessen wird Version {0} installiert."
|
||||
-- AI Studio couldn't install Pandoc.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio konnte Pandoc nicht installieren."
|
||||
|
||||
-- Pandoc is required for Microsoft Word export.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc wird für den Export nach Microsoft Word benötigt."
|
||||
@ -7503,6 +7670,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701
|
||||
-- Pandoc may be required for importing files.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Zum Importieren von Dateien kann Pandoc erforderlich sein."
|
||||
|
||||
-- Failed to store the secret data due to an API issue.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Fehler beim Speichern der geheimen Daten aufgrund eines API-Problems."
|
||||
|
||||
-- Failed to delete the secret data due to an API issue.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Das Löschen der geheimen Daten ist aufgrund eines API-Problems fehlgeschlagen."
|
||||
|
||||
@ -7632,6 +7802,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T29806295
|
||||
-- Images are not supported at this place
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T305247150"] = "Bilder werden an dieser Stelle nicht unterstützt."
|
||||
|
||||
-- This file format is not supported. Please convert the .doc file to .docx (e.g. with Microsoft Word).
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T3740637731"] = "Dieses Dateiformat wird nicht unterstützt. Bitte konvertieren Sie die .doc-Datei in eine .docx-Datei (z. B. mit Microsoft Word)."
|
||||
|
||||
-- Unsupported file type
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T4041351522"] = "Nicht unterstützter Dateityp"
|
||||
|
||||
|
||||
@ -2649,6 +2649,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1599198973"]
|
||||
-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."
|
||||
|
||||
-- seconds
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds"
|
||||
|
||||
-- Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1834486728"] = "Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled."
|
||||
|
||||
@ -2697,6 +2700,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"]
|
||||
-- Spellchecking is enabled
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3165555978"] = "Spellchecking is enabled"
|
||||
|
||||
-- Request timeout
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3569531009"] = "Request timeout"
|
||||
|
||||
-- App Options
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3577148634"] = "App Options"
|
||||
|
||||
@ -2724,6 +2730,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"]
|
||||
-- Select a transcription provider
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4174666315"] = "Select a transcription provider"
|
||||
|
||||
-- How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4192032183"] = "How long AI Studio waits for external HTTP requests, such as AI providers, embeddings, transcription, ERI data sources, and enterprise configuration downloads."
|
||||
|
||||
-- Navigation bar behavior
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"] = "Navigation bar behavior"
|
||||
|
||||
@ -3123,6 +3132,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2372624045"] = "Start rec
|
||||
-- Transcription in progress...
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T2851219233"] = "Transcription in progress..."
|
||||
|
||||
-- Unfortunately, there was an error communicating with the AI system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T3236134591"] = "Unfortunately, there was an error communicating with the AI system."
|
||||
|
||||
-- The configured transcription provider was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::VOICERECORDER::T331613105"] = "The configured transcription provider was not found."
|
||||
|
||||
@ -3636,6 +3648,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2879113658"] =
|
||||
-- Maximum matches per query
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximum matches per query"
|
||||
|
||||
-- Failed to read the user's username from the operating system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2909734556"] = "Failed to read the user's username from the operating system."
|
||||
|
||||
-- Open web link, show more information
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Open web link, show more information"
|
||||
|
||||
@ -3687,6 +3702,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T742006305"] = "
|
||||
-- Embeddings
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Embeddings"
|
||||
|
||||
-- Use the same username and password for all users
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1769874785"] = "Use the same username and password for all users"
|
||||
|
||||
-- Username and password mode
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1787063064"] = "Username and password mode"
|
||||
|
||||
-- How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3081234668"] = "How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?"
|
||||
|
||||
-- User-managed username and password
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T365340972"] = "User-managed username and password"
|
||||
|
||||
-- Export
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3898821075"] = "Export"
|
||||
|
||||
-- Read each user's username from the operating system and share one password
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T76405695"] = "Read each user's username from the operating system and share one password"
|
||||
|
||||
-- Cancel
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T900713019"] = "Cancel"
|
||||
|
||||
-- Describe what data this directory contains to help the AI select it.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Describe what data this directory contains to help the AI select it."
|
||||
|
||||
@ -4722,6 +4758,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T582516016"] =
|
||||
-- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them."
|
||||
|
||||
-- Copy attachments into plugin
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1345613295"] = "Copy attachments into plugin"
|
||||
|
||||
-- Delete
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Delete"
|
||||
|
||||
@ -4731,6 +4770,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T15483
|
||||
-- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts."
|
||||
|
||||
-- Use shared attachment paths
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2054531878"] = "Use shared attachment paths"
|
||||
|
||||
-- No chat templates configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet."
|
||||
|
||||
@ -4749,6 +4791,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T34481
|
||||
-- This template is managed by your organization.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization."
|
||||
|
||||
-- Select configuration plugin folder
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576816894"] = "Select configuration plugin folder"
|
||||
|
||||
-- Edit Chat Template
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template"
|
||||
|
||||
@ -4761,6 +4806,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38650
|
||||
-- Delete Chat Template
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Delete Chat Template"
|
||||
|
||||
-- Export Chat Template
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Export Chat Template"
|
||||
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T975426229"] = "Export configuration"
|
||||
|
||||
-- Which programming language should be preselected for added contexts?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Which programming language should be preselected for added contexts?"
|
||||
|
||||
@ -4815,6 +4866,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T145419
|
||||
-- Delete
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Delete"
|
||||
|
||||
-- Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1577531115"] = "Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."
|
||||
|
||||
-- Cannot export this ERI data source because the authentication secret could not be encrypted.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1592527757"] = "Cannot export this ERI data source because the authentication secret could not be encrypted."
|
||||
|
||||
-- External (ERI)
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "External (ERI)"
|
||||
|
||||
@ -4845,6 +4902,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T269820
|
||||
-- Embedding
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Embedding"
|
||||
|
||||
-- This data source is managed by your organization.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3031462878"] = "This data source is managed by your organization."
|
||||
|
||||
-- Edit
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Edit"
|
||||
|
||||
@ -4869,21 +4929,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T352566
|
||||
-- No data sources configured yet.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "No data sources configured yet."
|
||||
|
||||
-- Export Access Token?
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3595669127"] = "Export Access Token?"
|
||||
|
||||
-- Export ERI Data Source
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3831281036"] = "Export ERI Data Source"
|
||||
|
||||
-- Actions
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Actions"
|
||||
|
||||
-- This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T4027572258"] = "This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token."
|
||||
|
||||
-- Configured Data Sources
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Configured Data Sources"
|
||||
|
||||
-- Add ERI v1 Data Source
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "Add ERI v1 Data Source"
|
||||
|
||||
-- Cannot export this ERI data source because no enterprise encryption secret is configured.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T750361472"] = "Cannot export this ERI data source because no enterprise encryption secret is configured."
|
||||
|
||||
-- External Data (ERI-Server v1)
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "External Data (ERI-Server v1)"
|
||||
|
||||
-- Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T782820095"] = "Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"
|
||||
|
||||
-- Local Directory
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Local Directory"
|
||||
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T975426229"] = "Export configuration"
|
||||
|
||||
-- When enabled, you can preselect some ERI server options.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "When enabled, you can preselect some ERI server options."
|
||||
|
||||
@ -5169,6 +5247,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T55364659"
|
||||
-- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile.
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile."
|
||||
|
||||
-- Export configuration
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T975426229"] = "Export configuration"
|
||||
|
||||
-- Preselect the target language
|
||||
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Preselect the target language"
|
||||
|
||||
@ -6027,18 +6108,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1890416390"] = "Check for update
|
||||
-- Vision
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision"
|
||||
|
||||
-- In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library."
|
||||
|
||||
-- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."
|
||||
|
||||
-- Encryption secret: is configured
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured"
|
||||
|
||||
-- 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"
|
||||
|
||||
@ -6066,6 +6141,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2301484629"] = "Configuration pl
|
||||
-- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2329884315"] = "The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK."
|
||||
|
||||
-- Linux AppImages bundle GStreamer components to support microphone access and WebM audio recording in the embedded WebKitGTK web view.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T234598990"] = "Linux AppImages bundle GStreamer components to support microphone access and WebM audio recording in the embedded WebKitGTK web view."
|
||||
|
||||
-- Used PDFium version
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2368247719"] = "Used PDFium version"
|
||||
|
||||
@ -6120,6 +6198,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Used .NET runtim
|
||||
-- Explanation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation"
|
||||
|
||||
-- checking availability
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "checking availability"
|
||||
|
||||
-- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software."
|
||||
|
||||
@ -6141,6 +6222,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ide
|
||||
-- Hide Details
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details"
|
||||
|
||||
-- Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface."
|
||||
|
||||
-- Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3239817808"] = "Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running."
|
||||
|
||||
-- Update Pandoc
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Update Pandoc"
|
||||
|
||||
@ -6165,6 +6252,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs w
|
||||
-- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!"
|
||||
|
||||
-- AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service."
|
||||
|
||||
-- Motivation
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation"
|
||||
|
||||
@ -6174,6 +6264,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available"
|
||||
-- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat."
|
||||
|
||||
-- Username provided by the OS
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS"
|
||||
|
||||
-- this version does not met the requirements
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements"
|
||||
|
||||
@ -6195,6 +6288,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions"
|
||||
-- Database
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database"
|
||||
|
||||
-- This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication."
|
||||
|
||||
-- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system."
|
||||
|
||||
@ -6215,6 +6311,8 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK"
|
||||
|
||||
-- We use the DeepSeek Tokenizer to estimate the number of tokens an input will generate.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T591393704"] = "We use the DeepSeek Tokenizer to estimate the number of tokens an input will generate."
|
||||
-- starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "starting"
|
||||
|
||||
-- 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."
|
||||
@ -6237,6 +6335,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Provided by confi
|
||||
-- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate."
|
||||
|
||||
-- Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running."
|
||||
|
||||
-- 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."
|
||||
|
||||
@ -6378,6 +6479,15 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::WRITER::T779923726"] = "Your stage directions"
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1000247110"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The server might be down or having issues. The provider message is: '{2}'"
|
||||
|
||||
-- The provider '{0}' reported an error while streaming the response.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1008706234"] = "The provider '{0}' reported an error while streaming the response."
|
||||
|
||||
-- The provider rejected the request because too many requests were sent. Please wait a moment and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1028424693"] = "The provider rejected the request because too many requests were sent. Please wait a moment and try again."
|
||||
|
||||
-- The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1069211263"] = "The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding."
|
||||
|
||||
-- Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T1487597412"] = "Tried to stream the LLM provider '{0}' answer. There were some problems with the stream. The message is: '{1}'"
|
||||
|
||||
@ -6408,6 +6518,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T3759732886"] = "We tried to
|
||||
-- We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T4049517041"] = "We tried to communicate with the LLM provider '{0}' (type={1}). The data of the chat, including all file attachments, is probably too large for the selected model and provider. The provider message is: '{2}'"
|
||||
|
||||
-- The provider '{0}' reported an error: {1}
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::BASEPROVIDER::T700894460"] = "The provider '{0}' reported an error: {1}"
|
||||
|
||||
-- The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::CONFIDENCE::T1014558951"] = "The trust level of this provider **has not yet** been thoroughly **investigated and evaluated**. We do not know if your data is safe."
|
||||
|
||||
@ -6468,6 +6581,9 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODEL::T2234274832"] = "no model selected"
|
||||
-- We could not load models from '{0}'. The account or API key does not have the required permissions.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T1143085203"] = "We could not load models from '{0}'. The account or API key does not have the required permissions."
|
||||
|
||||
-- We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T155481725"] = "We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again."
|
||||
|
||||
-- We could not load models from '{0}'. The API key is probably missing, invalid, or expired.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2041046579"] = "We could not load models from '{0}'. The API key is probably missing, invalid, or expired."
|
||||
|
||||
@ -6477,15 +6593,39 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T21156887
|
||||
-- We could not load models from '{0}' because the provider returned an unexpected response.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T2186844789"] = "We could not load models from '{0}' because the provider returned an unexpected response."
|
||||
|
||||
-- We could not load models from '{0}' because the account appears to have no API credits left.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T373339048"] = "We could not load models from '{0}' because the account appears to have no API credits left."
|
||||
|
||||
-- We could not load models from '{0}' due to an unknown error.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T3907712809"] = "We could not load models from '{0}' due to an unknown error."
|
||||
|
||||
-- It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again.
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::OPENAI::PROVIDEROPENAI::T757371511"] = "It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again."
|
||||
|
||||
-- Model as configured by whisper.cpp
|
||||
UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp"
|
||||
|
||||
-- Cannot export this chat template because example message {0} is not a text message.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Cannot export this chat template because example message {0} is not a text message."
|
||||
|
||||
-- Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2407395493"] = "Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins."
|
||||
|
||||
-- Please select a valid configuration plugin folder. The folder must contain a plugin.lua file.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2542895569"] = "Please select a valid configuration plugin folder. The folder must contain a plugin.lua file."
|
||||
|
||||
-- Cannot package the chat template attachments. The issue was: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T3635593138"] = "Cannot package the chat template attachments. The issue was: {0}"
|
||||
|
||||
-- Cannot package the attachment '{0}' because the file does not exist.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4121340492"] = "Cannot package the attachment '{0}' because the file does not exist."
|
||||
|
||||
-- Use no chat template
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template"
|
||||
|
||||
-- Cannot export this chat template because example message {0} is empty.
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T477540958"] = "Cannot export this chat template because example message {0} is empty."
|
||||
|
||||
-- Navigation never expands, but there are tooltips
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Navigation never expands, but there are tooltips"
|
||||
|
||||
@ -6681,8 +6821,8 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2708
|
||||
-- Unknown preview feature
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unknown preview feature"
|
||||
|
||||
-- Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T714355911"] = "Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text"
|
||||
-- Transcription: Convert recordings and audio files into text
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T4247148645"] = "Transcription: Convert recordings and audio files into text"
|
||||
|
||||
-- Use no data sources, when sending an assistant result to a chat
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Use no data sources, when sending an assistant result to a chat"
|
||||
@ -6708,6 +6848,21 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::THEMESEXTENSIONS::T534715610"] =
|
||||
-- Use no profile
|
||||
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::PROFILE::T2205839602"] = "Use no profile"
|
||||
|
||||
-- The selected model is not available.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T1578005752"] = "The selected model is not available."
|
||||
|
||||
-- The selected provider is not allowed for this chat.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T174545104"] = "The selected provider is not allowed for this chat."
|
||||
|
||||
-- The AI job failed. The message is: '{0}'
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T237448388"] = "The AI job failed. The message is: '{0}'"
|
||||
|
||||
-- The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T3267850764"] = "The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings."
|
||||
|
||||
-- We could load models from '{0}', but the provider did not return any usable text models.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AIJOBS::AIJOBSERVICE::T3378120620"] = "We could load models from '{0}', but the provider did not return any usable text models."
|
||||
|
||||
-- SSO (Kerberos)
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::AUTHMETHODSV1EXTENSIONS::T268552140"] = "SSO (Kerberos)"
|
||||
|
||||
@ -6849,6 +7004,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = "
|
||||
-- Reason
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason"
|
||||
|
||||
-- Starting
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Starting"
|
||||
|
||||
-- Unavailable
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable"
|
||||
|
||||
@ -6933,6 +7091,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2858189239"] = "Faile
|
||||
-- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout."
|
||||
|
||||
-- Failed to read the user's username from the operating system.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2909734556"] = "Failed to read the user's username from the operating system."
|
||||
|
||||
-- Failed to retrieve the security requirements due to an exception: {0}
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Failed to retrieve the security requirements due to an exception: {0}"
|
||||
|
||||
@ -6978,6 +7139,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Failed
|
||||
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
|
||||
|
||||
-- AI Studio couldn't install Pandoc because the archive was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found."
|
||||
|
||||
-- Pandoc doesn't seem to be installed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc doesn't seem to be installed."
|
||||
|
||||
-- Was not able to validate the Pandoc installation.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Was not able to validate the Pandoc installation."
|
||||
|
||||
@ -6999,20 +7166,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} is instal
|
||||
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} is installed, but it does not match the required version (v{1})."
|
||||
|
||||
-- Pandoc was not installed successfully, because the archive was not found.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc was not installed successfully, because the archive was not found."
|
||||
-- AI Studio couldn't install Pandoc because the archive type is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio couldn't install Pandoc because the archive type is unknown."
|
||||
|
||||
-- Pandoc is not available on the system or the process had issues.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not available on the system or the process had issues."
|
||||
|
||||
-- Pandoc was not installed successfully, because the archive type is unknown.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc was not installed successfully, because the archive type is unknown."
|
||||
-- AI Studio couldn't install Pandoc because the executable was not found in the archive.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio couldn't install Pandoc because the executable was not found in the archive."
|
||||
|
||||
-- It seems that Pandoc is not installed.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc is not installed."
|
||||
-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio couldn't find the latest Pandoc version and will install version {0} instead."
|
||||
|
||||
-- The latest Pandoc version was not found, installing version {0} instead.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead."
|
||||
-- AI Studio couldn't install Pandoc.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't install Pandoc."
|
||||
|
||||
-- Pandoc is required for Microsoft Word export.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export."
|
||||
@ -7503,6 +7670,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701
|
||||
-- Pandoc may be required for importing files.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files."
|
||||
|
||||
-- Failed to store the secret data due to an API issue.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue."
|
||||
|
||||
-- Failed to delete the secret data due to an API issue.
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue."
|
||||
|
||||
@ -7632,6 +7802,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T29806295
|
||||
-- Images are not supported at this place
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T305247150"] = "Images are not supported at this place"
|
||||
|
||||
-- This file format is not supported. Please convert the .doc file to .docx (e.g. with Microsoft Word).
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T3740637731"] = "This file format is not supported. Please convert the .doc file to .docx (e.g. with Microsoft Word)."
|
||||
|
||||
-- Unsupported file type
|
||||
UI_TEXT_CONTENT["AISTUDIO::TOOLS::VALIDATION::FILEEXTENSIONVALIDATION::T4041351522"] = "Unsupported file type"
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ using AIStudio.Agents;
|
||||
using AIStudio.Agents.AssistantAudit;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.Databases;
|
||||
using AIStudio.Tools.AIJobs;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.PluginSystem.Assistants;
|
||||
using AIStudio.Tools.Services;
|
||||
@ -132,6 +133,7 @@ internal sealed class Program
|
||||
builder.Services.AddMudMarkdownClipboardService<MarkdownClipboardService>();
|
||||
builder.Services.AddSingleton<SettingsManager>();
|
||||
builder.Services.AddSingleton<ThreadSafeRandom>();
|
||||
builder.Services.AddSingleton<AIJobService>();
|
||||
builder.Services.AddSingleton<VoiceRecordingAvailabilityService>();
|
||||
builder.Services.AddSingleton<DataSourceService>();
|
||||
builder.Services.AddSingleton<DataSourceEmbeddingService>();
|
||||
|
||||
@ -60,9 +60,9 @@ public sealed class ProviderAlibabaCloud() : BaseProvider(LLMProviders.ALIBABA_C
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
|
||||
@ -116,9 +116,9 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
@ -179,6 +179,7 @@ public sealed class ProviderAnthropic() : BaseProvider(LLMProviders.ANTHROPIC, "
|
||||
{
|
||||
System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
|
||||
System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
System.Net.HttpStatusCode.TooManyRequests => ModelLoadFailureReason.TOO_MANY_REQUESTS,
|
||||
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
|
||||
},
|
||||
requestConfigurator: (request, secretKey) =>
|
||||
|
||||
@ -29,7 +29,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
/// <summary>
|
||||
/// The HTTP client to use it for all requests.
|
||||
/// </summary>
|
||||
protected readonly HttpClient HttpClient = new();
|
||||
protected readonly HttpClient HttpClient = ExternalHttpClientTimeout.CreateHttpClient();
|
||||
|
||||
/// <summary>
|
||||
/// The logger to use.
|
||||
@ -103,7 +103,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
public abstract IAsyncEnumerable<ImageURL> StreamImageCompletion(Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, CancellationToken token = default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default);
|
||||
public abstract Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts);
|
||||
@ -139,6 +139,23 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
|
||||
protected static ModelLoadResult FailedModelLoadResult(ModelLoadFailureReason failureReason, string? technicalDetails = null) => ModelLoadResult.Failure(failureReason, technicalDetails);
|
||||
|
||||
protected bool IsTimeoutException(Exception exception, CancellationToken token = default)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
return ExternalHttpClientTimeout.IsTimeoutException(exception, token);
|
||||
}
|
||||
|
||||
protected Task SendTimeoutError(string action) => MessageBus.INSTANCE.SendError(new(
|
||||
Icons.Material.Filled.HourglassTop,
|
||||
string.Format(
|
||||
TB("The request to the LLM provider '{0}' (type={1}) timed out after {2} while {3}. Please try again or check whether the provider is still responding."),
|
||||
this.InstanceName,
|
||||
this.Provider,
|
||||
ExternalHttpClientTimeout.GetTimeoutDescription(),
|
||||
action)));
|
||||
|
||||
protected async Task<string?> GetModelLoadingSecretKey(SecretStoreType storeType, string? apiKeyProvisional = null, bool isTryingSecret = false) => apiKeyProvisional switch
|
||||
{
|
||||
not null => apiKeyProvisional,
|
||||
@ -153,10 +170,18 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
{
|
||||
HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
|
||||
HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
HttpStatusCode.TooManyRequests => ModelLoadFailureReason.TOO_MANY_REQUESTS,
|
||||
|
||||
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
|
||||
};
|
||||
|
||||
protected ModelLoadFailureReason GetModelLoadFailureReason(HttpResponseMessage response, string responseBody) => this.ClassifyProviderRequestFailure(response.StatusCode, responseBody) switch
|
||||
{
|
||||
ProviderRequestFailureReason.INSUFFICIENT_QUOTA => ModelLoadFailureReason.INSUFFICIENT_QUOTA,
|
||||
ProviderRequestFailureReason.TOO_MANY_REQUESTS => ModelLoadFailureReason.TOO_MANY_REQUESTS,
|
||||
_ => GetDefaultModelLoadFailureReason(response),
|
||||
};
|
||||
|
||||
protected async Task<ModelLoadResult> LoadModelsResponse<TResponse>(
|
||||
SecretStoreType storeType,
|
||||
string requestPath,
|
||||
@ -178,42 +203,220 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
else if (!string.IsNullOrWhiteSpace(secretKey))
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
try
|
||||
{
|
||||
var failureReason = failureReasonSelector?.Invoke(response, responseBody) ?? GetDefaultModelLoadFailureReason(response);
|
||||
return FailedModelLoadResult(failureReason, $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{responseBody}'");
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var failureReason = failureReasonSelector?.Invoke(response, responseBody) ?? this.GetModelLoadFailureReason(response, responseBody);
|
||||
this.logger.LogError("Model loading request failed with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", response.StatusCode, response.ReasonPhrase, responseBody);
|
||||
return FailedModelLoadResult(failureReason, $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{responseBody}'");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parsedResponse = JsonSerializer.Deserialize<TResponse>(responseBody, jsonSerializerOptions ?? JSON_SERIALIZER_OPTIONS);
|
||||
if (parsedResponse is null)
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, "Model list response could not be deserialized.");
|
||||
|
||||
return SuccessfulModelLoadResult(modelFactory(parsedResponse));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, e.Message);
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (this.IsTimeoutException(e, token))
|
||||
{
|
||||
await this.SendTimeoutError("loading the available models");
|
||||
this.logger.LogError(e, "Timed out while loading models from provider '{ProviderInstanceName}' (provider={ProviderType}).", this.InstanceName, this.Provider);
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason) => failureReason switch
|
||||
{
|
||||
ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("The provider rejected the request because too many requests were sent. Please wait a moment and try again."),
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
protected virtual ProviderRequestFailureReason ClassifyProviderRequestFailure(HttpStatusCode statusCode, string responseBody)
|
||||
{
|
||||
if (statusCode is not HttpStatusCode.TooManyRequests)
|
||||
return ProviderRequestFailureReason.NONE;
|
||||
|
||||
return ProviderRequestFailureReason.TOO_MANY_REQUESTS;
|
||||
}
|
||||
|
||||
protected virtual ProviderRequestFailureReason ClassifyProviderRequestFailure(string? errorCode, string? errorType, string? errorMessage, string responseBody)
|
||||
{
|
||||
if (IsTooManyRequestsError(errorCode) || IsTooManyRequestsError(errorType) || IsTooManyRequestsError(errorMessage))
|
||||
return ProviderRequestFailureReason.TOO_MANY_REQUESTS;
|
||||
|
||||
return ProviderRequestFailureReason.NONE;
|
||||
}
|
||||
|
||||
private static bool IsTooManyRequestsError(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
return value.Equals("rate_limit_exceeded", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("too_many_requests", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("too_many_request", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Contains("too many requests", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Contains("rate limit", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Contains("rate_limit", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Contains("throttl", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private bool TryCreateProviderRequestExceptionFromStreamLine(string providerName, string line, out ProviderRequestException exception)
|
||||
{
|
||||
exception = new();
|
||||
|
||||
if (!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
return false;
|
||||
|
||||
var jsonData = line[6..].Trim();
|
||||
if (string.IsNullOrWhiteSpace(jsonData) || jsonData is "[DONE]")
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var parsedResponse = JsonSerializer.Deserialize<TResponse>(responseBody, jsonSerializerOptions ?? JSON_SERIALIZER_OPTIONS);
|
||||
if (parsedResponse is null)
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, "Model list response could not be deserialized.");
|
||||
using var document = JsonDocument.Parse(jsonData);
|
||||
var root = document.RootElement;
|
||||
if (!IsProviderStreamFailure(root))
|
||||
return false;
|
||||
|
||||
return SuccessfulModelLoadResult(modelFactory(parsedResponse));
|
||||
var eventType = TryGetString(root, "type");
|
||||
TryGetProviderStreamError(root, out var errorCode, out var errorType, out var errorMessage);
|
||||
var failureReason = this.ClassifyProviderRequestFailure(errorCode, errorType, errorMessage, jsonData);
|
||||
var userMessage = this.GetProviderRequestFailureUserMessage(failureReason);
|
||||
if (string.IsNullOrWhiteSpace(userMessage))
|
||||
{
|
||||
userMessage = string.IsNullOrWhiteSpace(errorMessage)
|
||||
? string.Format(TB("The provider '{0}' reported an error while streaming the response."), this.InstanceName)
|
||||
: string.Format(TB("The provider '{0}' reported an error: {1}"), this.InstanceName, errorMessage);
|
||||
}
|
||||
|
||||
this.logger.LogError("The {ProviderName} stream returned an error for provider '{ProviderInstanceName}' (provider={ProviderType}). EventType={StreamEventType}, ErrorCode={ErrorCode}, ErrorType={ErrorType}, ErrorMessage='{ErrorMessage}', Body='{ErrorBody}'", providerName, this.InstanceName, this.Provider, eventType, errorCode, errorType, errorMessage, jsonData);
|
||||
exception = new ProviderRequestException(failureReason, userMessage, responseBody: jsonData);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
catch (JsonException)
|
||||
{
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsProviderStreamFailure(JsonElement root)
|
||||
{
|
||||
var eventType = TryGetString(root, "type");
|
||||
if (eventType is not null && (
|
||||
eventType.Equals("error", StringComparison.OrdinalIgnoreCase) ||
|
||||
eventType.Equals("response.error", StringComparison.OrdinalIgnoreCase) ||
|
||||
eventType.Equals("response.failed", StringComparison.OrdinalIgnoreCase)))
|
||||
return true;
|
||||
|
||||
if (HasObjectProperty(root, "error"))
|
||||
return true;
|
||||
|
||||
if (IsTooManyRequestsError(TryGetString(root, "code")) ||
|
||||
IsTooManyRequestsError(TryGetString(root, "type")) ||
|
||||
IsTooManyRequestsError(TryGetString(root, "message")))
|
||||
return true;
|
||||
|
||||
if (TryGetString(root, "message") is not null &&
|
||||
(TryGetString(root, "code") is not null || TryGetString(root, "type") is not null) &&
|
||||
!root.TryGetProperty("choices", out _) &&
|
||||
!root.TryGetProperty("delta", out _))
|
||||
return true;
|
||||
|
||||
if (!root.TryGetProperty("response", out var responseElement) || responseElement.ValueKind is not JsonValueKind.Object)
|
||||
return false;
|
||||
|
||||
if (HasObjectProperty(responseElement, "error"))
|
||||
return true;
|
||||
|
||||
var responseStatus = TryGetString(responseElement, "status");
|
||||
return responseStatus is not null && responseStatus.Equals("failed", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool HasObjectProperty(JsonElement element, string propertyName)
|
||||
{
|
||||
return element.ValueKind is JsonValueKind.Object &&
|
||||
element.TryGetProperty(propertyName, out var propertyElement) &&
|
||||
propertyElement.ValueKind is JsonValueKind.Object;
|
||||
}
|
||||
|
||||
private static void TryGetProviderStreamError(JsonElement root, out string? errorCode, out string? errorType, out string? errorMessage)
|
||||
{
|
||||
errorCode = null;
|
||||
errorType = null;
|
||||
errorMessage = null;
|
||||
|
||||
if (TryGetErrorElement(root, out var errorElement))
|
||||
{
|
||||
errorCode = TryGetString(errorElement, "code");
|
||||
errorType = TryGetString(errorElement, "type");
|
||||
errorMessage = TryGetString(errorElement, "message");
|
||||
return;
|
||||
}
|
||||
|
||||
errorCode = TryGetString(root, "code");
|
||||
errorType = TryGetString(root, "type");
|
||||
errorMessage = TryGetString(root, "message");
|
||||
}
|
||||
|
||||
private static bool TryGetErrorElement(JsonElement root, out JsonElement errorElement)
|
||||
{
|
||||
if (root.ValueKind is JsonValueKind.Object &&
|
||||
root.TryGetProperty("error", out errorElement) &&
|
||||
errorElement.ValueKind is JsonValueKind.Object)
|
||||
return true;
|
||||
|
||||
if (root.ValueKind is JsonValueKind.Object &&
|
||||
root.TryGetProperty("response", out var responseElement) &&
|
||||
responseElement.ValueKind is JsonValueKind.Object &&
|
||||
responseElement.TryGetProperty("error", out errorElement) &&
|
||||
errorElement.ValueKind is JsonValueKind.Object)
|
||||
return true;
|
||||
|
||||
errorElement = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string? TryGetString(JsonElement element, string propertyName)
|
||||
{
|
||||
if (element.ValueKind is not JsonValueKind.Object ||
|
||||
!element.TryGetProperty(propertyName, out var propertyElement) ||
|
||||
propertyElement.ValueKind is not JsonValueKind.String)
|
||||
return null;
|
||||
|
||||
return propertyElement.GetString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a request and handles rate limiting by exponential backoff.
|
||||
/// </summary>
|
||||
/// <param name="requestBuilder">A function that builds the request.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <param name="userCancellationToken">The user cancellation token.</param>
|
||||
/// <param name="requestCancellationToken">The token to use for the HTTP request.</param>
|
||||
/// <returns>The status object of the request.</returns>
|
||||
private async Task<HttpRateLimitedStreamResult> SendRequest(Func<Task<HttpRequestMessage>> requestBuilder, CancellationToken token = default)
|
||||
private async Task<HttpRateLimitedStreamResult> SendRequest(Func<Task<HttpRequestMessage>> requestBuilder, CancellationToken userCancellationToken = default, CancellationToken requestCancellationToken = default)
|
||||
{
|
||||
const int MAX_RETRIES = 6;
|
||||
const double RETRY_DELAY_SECONDS = 4;
|
||||
var effectiveCancellationToken = requestCancellationToken.CanBeCanceled ? requestCancellationToken : userCancellationToken;
|
||||
|
||||
var retry = 0;
|
||||
var response = default(HttpResponseMessage);
|
||||
var errorMessage = string.Empty;
|
||||
var lastProviderRequestFailure = ProviderRequestFailureReason.NONE;
|
||||
HttpStatusCode? lastResponseStatusCode = null;
|
||||
var lastResponseReasonPhrase = string.Empty;
|
||||
var lastErrorBody = string.Empty;
|
||||
while (retry++ < MAX_RETRIES)
|
||||
{
|
||||
using var request = await requestBuilder();
|
||||
@ -226,14 +429,39 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
// Please notice: We do not dispose the response here. The caller is responsible
|
||||
// for disposing the response object. This is important because the response
|
||||
// object is used to read the stream.
|
||||
var nextResponse = await this.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
|
||||
HttpResponseMessage nextResponse;
|
||||
try
|
||||
{
|
||||
nextResponse = await this.HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, effectiveCancellationToken);
|
||||
}
|
||||
catch (Exception e) when (this.IsTimeoutException(e, userCancellationToken))
|
||||
{
|
||||
await this.SendTimeoutError("waiting for the chat response");
|
||||
this.logger.LogError(e, "Timed out while sending a streaming request to provider '{ProviderInstanceName}' (provider={ProviderType}).", this.InstanceName, this.Provider);
|
||||
return new HttpRateLimitedStreamResult(false, true, e.Message, response);
|
||||
}
|
||||
|
||||
if (nextResponse.IsSuccessStatusCode)
|
||||
{
|
||||
response = nextResponse;
|
||||
errorMessage = string.Empty;
|
||||
lastProviderRequestFailure = ProviderRequestFailureReason.NONE;
|
||||
break;
|
||||
}
|
||||
|
||||
var errorBody = await nextResponse.Content.ReadAsStringAsync(token);
|
||||
var errorBody = await nextResponse.Content.ReadAsStringAsync(effectiveCancellationToken);
|
||||
lastResponseStatusCode = nextResponse.StatusCode;
|
||||
lastResponseReasonPhrase = nextResponse.ReasonPhrase ?? string.Empty;
|
||||
lastErrorBody = errorBody;
|
||||
var providerRequestFailure = this.ClassifyProviderRequestFailure(nextResponse.StatusCode, errorBody);
|
||||
lastProviderRequestFailure = providerRequestFailure;
|
||||
if (providerRequestFailure is ProviderRequestFailureReason.INSUFFICIENT_QUOTA)
|
||||
{
|
||||
var userMessage = this.GetProviderRequestFailureUserMessage(providerRequestFailure);
|
||||
this.logger.LogError("Failed request with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", nextResponse.StatusCode, nextResponse.ReasonPhrase, errorBody);
|
||||
throw new ProviderRequestException(providerRequestFailure, userMessage, nextResponse.StatusCode, nextResponse.ReasonPhrase ?? string.Empty, errorBody);
|
||||
}
|
||||
|
||||
if (nextResponse.StatusCode is HttpStatusCode.Forbidden)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Block, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). You might not be able to use this provider from your location. The provider message is: '{2}'"), this.InstanceName, this.Provider, nextResponse.ReasonPhrase)));
|
||||
@ -299,11 +527,18 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
timeSeconds = 90;
|
||||
|
||||
this.logger.LogDebug("Failed request with status code {ResponseStatusCode} (message = '{ErrorMessage}'). Retrying in {TimeSeconds:0.00} seconds.", nextResponse.StatusCode, errorMessage, timeSeconds);
|
||||
await Task.Delay(TimeSpan.FromSeconds(timeSeconds), token);
|
||||
await Task.Delay(TimeSpan.FromSeconds(timeSeconds), effectiveCancellationToken);
|
||||
}
|
||||
|
||||
if(retry >= MAX_RETRIES || !string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
if (lastProviderRequestFailure is not ProviderRequestFailureReason.NONE)
|
||||
{
|
||||
var userMessage = this.GetProviderRequestFailureUserMessage(lastProviderRequestFailure);
|
||||
this.logger.LogError("The request to provider '{ProviderInstanceName}' (provider={ProviderType}) failed after {MaxRetries} retries with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}'): {ErrorMessage}", this.InstanceName, this.Provider, MAX_RETRIES, lastResponseStatusCode, lastResponseReasonPhrase, lastErrorBody, userMessage);
|
||||
throw new ProviderRequestException(lastProviderRequestFailure, userMessage, lastResponseStatusCode, lastResponseReasonPhrase, lastErrorBody);
|
||||
}
|
||||
|
||||
await MessageBus.INSTANCE.SendError(new DataErrorMessage(Icons.Material.Filled.CloudOff, string.Format(TB("We tried to communicate with the LLM provider '{0}' (type={1}). Even after {2} retries, there were some problems with the request. The provider message is: '{3}'."), this.InstanceName, this.Provider, MAX_RETRIES, errorMessage)));
|
||||
return new HttpRateLimitedStreamResult(false, true, errorMessage ?? $"Failed after {MAX_RETRIES} retries; no provider message available", response);
|
||||
}
|
||||
@ -326,10 +561,12 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
var annotationSupported = typeof(TAnnotation) != typeof(NoResponsesAnnotationStreamLine) && typeof(TAnnotation) != typeof(NoChatCompletionAnnotationStreamLine);
|
||||
|
||||
StreamReader? streamReader = null;
|
||||
using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token);
|
||||
var timeoutToken = timeoutTokenSource.Token;
|
||||
try
|
||||
{
|
||||
// Send the request using exponential backoff:
|
||||
var responseData = await this.SendRequest(requestBuilder, token);
|
||||
var responseData = await this.SendRequest(requestBuilder, token, timeoutToken);
|
||||
if(responseData.IsFailedAfterAllRetries)
|
||||
{
|
||||
this.logger.LogError($"The {providerName} chat completion failed: {responseData.ErrorMessage}");
|
||||
@ -337,15 +574,31 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
}
|
||||
|
||||
// Open the response stream:
|
||||
var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
|
||||
var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(timeoutToken);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
streamReader = new StreamReader(providerStream);
|
||||
}
|
||||
catch(ProviderRequestException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"), this.InstanceName, e.Message)));
|
||||
this.logger.LogError($"Failed to stream chat completion from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
this.logger.LogWarning("The user canceled the chat completion request for {ProviderName} '{ProviderInstanceName}' before the response stream was opened.", providerName, this.InstanceName);
|
||||
}
|
||||
else if (this.IsTimeoutException(e, token))
|
||||
{
|
||||
await this.SendTimeoutError("opening the chat response stream");
|
||||
this.logger.LogError(e, "Timed out while opening the chat completion stream from {ProviderName} '{ProviderInstanceName}'.", providerName, this.InstanceName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"), this.InstanceName, e.Message)));
|
||||
this.logger.LogError($"Failed to stream chat completion from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (streamReader is null)
|
||||
@ -367,7 +620,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
this.logger.LogWarning($"Failed to read the end-of-stream state from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Check if the token is canceled:
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@ -382,19 +635,38 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
string? line;
|
||||
try
|
||||
{
|
||||
line = await streamReader.ReadLineAsync(token);
|
||||
line = await streamReader.ReadLineAsync(timeoutToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"), this.InstanceName, e.Message)));
|
||||
this.logger.LogError($"Failed to read the stream from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
this.logger.LogWarning("The user canceled the chat completion stream for {ProviderName} '{ProviderInstanceName}' while reading the next chunk.", providerName, this.InstanceName);
|
||||
}
|
||||
else if (this.IsTimeoutException(e, token))
|
||||
{
|
||||
await this.SendTimeoutError("reading the chat response stream");
|
||||
this.logger.LogError(e, "Timed out while reading the chat stream from {ProviderName} '{ProviderInstanceName}'.", providerName, this.InstanceName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"), this.InstanceName, e.Message)));
|
||||
this.logger.LogError($"Failed to read the stream from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (line is null)
|
||||
break;
|
||||
|
||||
// Skip empty lines:
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
if (this.TryCreateProviderRequestExceptionFromStreamLine(providerName, line, out var providerRequestException))
|
||||
throw providerRequestException;
|
||||
|
||||
// Skip lines that do not start with "data: ". Regard
|
||||
// to the specification, we only want to read the data lines:
|
||||
if (!line.StartsWith("data: ", StringComparison.InvariantCulture))
|
||||
@ -490,10 +762,12 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
var annotationSupported = typeof(TAnnotation) != typeof(NoResponsesAnnotationStreamLine) && typeof(TAnnotation) != typeof(NoChatCompletionAnnotationStreamLine);
|
||||
|
||||
StreamReader? streamReader = null;
|
||||
using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token);
|
||||
var timeoutToken = timeoutTokenSource.Token;
|
||||
try
|
||||
{
|
||||
// Send the request using exponential backoff:
|
||||
var responseData = await this.SendRequest(requestBuilder, token);
|
||||
var responseData = await this.SendRequest(requestBuilder, token, timeoutToken);
|
||||
if(responseData.IsFailedAfterAllRetries)
|
||||
{
|
||||
this.logger.LogError($"The {providerName} responses call failed: {responseData.ErrorMessage}");
|
||||
@ -501,15 +775,31 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
}
|
||||
|
||||
// Open the response stream:
|
||||
var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(token);
|
||||
var providerStream = await responseData.Response!.Content.ReadAsStreamAsync(timeoutToken);
|
||||
|
||||
// Add a stream reader to read the stream, line by line:
|
||||
streamReader = new StreamReader(providerStream);
|
||||
}
|
||||
catch(ProviderRequestException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"), this.InstanceName, e.Message)));
|
||||
this.logger.LogError($"Failed to stream responses from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
this.logger.LogWarning("The user canceled the responses request for {ProviderName} '{ProviderInstanceName}' before the response stream was opened.", providerName, this.InstanceName);
|
||||
}
|
||||
else if (this.IsTimeoutException(e, token))
|
||||
{
|
||||
await this.SendTimeoutError("opening the chat response stream");
|
||||
this.logger.LogError(e, "Timed out while opening the responses stream from {ProviderName} '{ProviderInstanceName}'.", providerName, this.InstanceName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to communicate with the LLM provider '{0}'. There were some problems with the request. The provider message is: '{1}'"), this.InstanceName, e.Message)));
|
||||
this.logger.LogError($"Failed to stream responses from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (streamReader is null)
|
||||
@ -531,7 +821,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
this.logger.LogWarning($"Failed to read the end-of-stream state from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Check if the token is canceled:
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
@ -546,19 +836,38 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
string? line;
|
||||
try
|
||||
{
|
||||
line = await streamReader.ReadLineAsync(token);
|
||||
line = await streamReader.ReadLineAsync(timeoutToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"), this.InstanceName, e.Message)));
|
||||
this.logger.LogError($"Failed to read the stream from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
this.logger.LogWarning("The user canceled the responses stream for {ProviderName} '{ProviderInstanceName}' while reading the next chunk.", providerName, this.InstanceName);
|
||||
}
|
||||
else if (this.IsTimeoutException(e, token))
|
||||
{
|
||||
await this.SendTimeoutError("reading the chat response stream");
|
||||
this.logger.LogError(e, "Timed out while reading the responses stream from {ProviderName} '{ProviderInstanceName}'.", providerName, this.InstanceName);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("Tried to stream the LLM provider '{0}' answer. Was not able to read the stream. The message is: '{1}'"), this.InstanceName, e.Message)));
|
||||
this.logger.LogError($"Failed to read the stream from {providerName} '{this.InstanceName}': {e.Message}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (line is null)
|
||||
break;
|
||||
|
||||
// Skip empty lines:
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
if (this.TryCreateProviderRequestExceptionFromStreamLine(providerName, line, out var providerRequestException))
|
||||
throw providerRequestException;
|
||||
|
||||
// Check if the line is the end of the stream:
|
||||
if (line.StartsWith("event: response.completed", StringComparison.InvariantCulture))
|
||||
yield break;
|
||||
@ -708,7 +1017,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
yield return content;
|
||||
}
|
||||
|
||||
protected async Task<string> PerformStandardTranscriptionRequest(RequestedSecret requestedSecret, Model transcriptionModel, string audioFilePath, Host host = Host.NONE, CancellationToken token = default)
|
||||
protected async Task<TranscriptionResult> PerformStandardTranscriptionRequest(RequestedSecret requestedSecret, Model transcriptionModel, string audioFilePath, Host host = Host.NONE, CancellationToken token = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -750,7 +1059,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
if(!requestedSecret.Success)
|
||||
{
|
||||
this.logger.LogError("No valid API key available for transcription request.");
|
||||
return string.Empty;
|
||||
return TranscriptionResult.Failure();
|
||||
}
|
||||
|
||||
request.Headers.Add("Authorization", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
@ -760,7 +1069,7 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
if(!requestedSecret.Success)
|
||||
{
|
||||
this.logger.LogError("No valid API key available for transcription request.");
|
||||
return string.Empty;
|
||||
return TranscriptionResult.Failure();
|
||||
}
|
||||
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION));
|
||||
@ -768,27 +1077,31 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
}
|
||||
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
var responseBody = response.Content.ReadAsStringAsync(token).Result;
|
||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger.LogError("Transcription request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
|
||||
return string.Empty;
|
||||
var providerRequestFailure = this.ClassifyProviderRequestFailure(response.StatusCode, responseBody);
|
||||
return TranscriptionResult.Failure(this.GetProviderRequestFailureUserMessage(providerRequestFailure));
|
||||
}
|
||||
|
||||
var transcriptionResponse = JsonSerializer.Deserialize<TranscriptionResponse>(responseBody, JSON_SERIALIZER_OPTIONS);
|
||||
if(transcriptionResponse is null)
|
||||
{
|
||||
this.logger.LogError("Was not able to deserialize the transcription response.");
|
||||
return string.Empty;
|
||||
return TranscriptionResult.Failure();
|
||||
}
|
||||
|
||||
return transcriptionResponse.Text;
|
||||
return TranscriptionResult.FromText(transcriptionResponse.Text);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.IsTimeoutException(e, token))
|
||||
await this.SendTimeoutError("transcribing audio");
|
||||
|
||||
this.logger.LogError("Failed to perform transcription request: '{Message}'.", e.Message);
|
||||
return string.Empty;
|
||||
return TranscriptionResult.Failure();
|
||||
}
|
||||
}
|
||||
|
||||
@ -838,11 +1151,16 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
// 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;
|
||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
this.logger.LogError("Embedding request failed with status code {ResponseStatusCode} and body: '{ResponseBody}'.", response.StatusCode, responseBody);
|
||||
var providerRequestFailure = this.ClassifyProviderRequestFailure(response.StatusCode, responseBody);
|
||||
var userMessage = this.GetProviderRequestFailureUserMessage(providerRequestFailure);
|
||||
if (!string.IsNullOrWhiteSpace(userMessage))
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, userMessage));
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -862,6 +1180,9 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.IsTimeoutException(e, token))
|
||||
await this.SendTimeoutError("creating embeddings");
|
||||
|
||||
this.logger.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
|
||||
return [];
|
||||
}
|
||||
@ -1016,4 +1337,4 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -60,9 +60,9 @@ public sealed class ProviderDeepSeek() : BaseProvider(LLMProviders.DEEP_SEEK, "h
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
|
||||
@ -61,7 +61,7 @@ public class ProviderFireworks() : BaseProvider(LLMProviders.FIREWORKS, "https:/
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override async Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER);
|
||||
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
||||
|
||||
@ -60,7 +60,7 @@ public sealed class ProviderGWDG() : BaseProvider(LLMProviders.GWDG, "https://ch
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override async Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER);
|
||||
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
||||
|
||||
@ -63,9 +63,9 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
@ -135,6 +135,9 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (this.IsTimeoutException(e, token))
|
||||
await this.SendTimeoutError("creating embeddings");
|
||||
|
||||
LOGGER.LogError("Failed to perform embedding request: '{Message}'.", e.Message);
|
||||
return [];
|
||||
}
|
||||
@ -197,6 +200,7 @@ public class ProviderGoogle() : BaseProvider(LLMProviders.GOOGLE, "https://gener
|
||||
{
|
||||
System.Net.HttpStatusCode.Forbidden => ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
System.Net.HttpStatusCode.Unauthorized => ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY,
|
||||
System.Net.HttpStatusCode.TooManyRequests => ModelLoadFailureReason.TOO_MANY_REQUESTS,
|
||||
_ => ModelLoadFailureReason.PROVIDER_UNAVAILABLE,
|
||||
});
|
||||
}
|
||||
|
||||
@ -64,9 +64,9 @@ public class ProviderGroq() : BaseProvider(LLMProviders.GROQ, "https://api.groq.
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
|
||||
@ -62,9 +62,9 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
@ -125,31 +125,40 @@ public sealed class ProviderHelmholtz() : BaseProvider(LLMProviders.HELMHOLTZ, "
|
||||
if (string.IsNullOrWhiteSpace(secretKey))
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, "No API key available for model loading.");
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
var body = await response.Content.ReadAsStringAsync(token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return FailedModelLoadResult(GetDefaultModelLoadFailureReason(response), $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{body}'");
|
||||
|
||||
try
|
||||
{
|
||||
var modelResponse = JsonSerializer.Deserialize<ModelsResponse>(body, JSON_SERIALIZER_OPTIONS);
|
||||
return SuccessfulModelLoadResult(modelResponse.Data);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var response = await this.HttpClient.SendAsync(request, token);
|
||||
var body = await response.Content.ReadAsStringAsync(token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return FailedModelLoadResult(GetDefaultModelLoadFailureReason(response), $"Status={(int)response.StatusCode} {response.ReasonPhrase}; Body='{body}'");
|
||||
|
||||
try
|
||||
{
|
||||
var modelResponse = JsonSerializer.Deserialize<ModelsResponse>(body, JSON_SERIALIZER_OPTIONS);
|
||||
return SuccessfulModelLoadResult(modelResponse.Data);
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
if (body.Contains("API key", StringComparison.InvariantCultureIgnoreCase))
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, body);
|
||||
|
||||
LOGGER.LogError(e, "Unexpected error while parsing models from Helmholtz API response. Status Code: {StatusCode}. Reason: {ReasonPhrase}. Response Body: '{ResponseBody}'", response.StatusCode, response.ReasonPhrase, body);
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, body);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.LogError(e, "Unexpected error while loading models from Helmholtz API. Status Code: {StatusCode}. Reason: {ReasonPhrase}", response.StatusCode, response.ReasonPhrase);
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.UNKNOWN, e.Message);
|
||||
}
|
||||
}
|
||||
catch (JsonException e)
|
||||
catch (Exception e) when (this.IsTimeoutException(e, token))
|
||||
{
|
||||
if (body.Contains("API key", StringComparison.InvariantCultureIgnoreCase))
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY, body);
|
||||
|
||||
LOGGER.LogError(e, "Unexpected error while parsing models from Helmholtz API response. Status Code: {StatusCode}. Reason: {ReasonPhrase}. Response Body: '{ResponseBody}'", response.StatusCode, response.ReasonPhrase, body);
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.INVALID_RESPONSE, body);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LOGGER.LogError(e, "Unexpected error while loading models from Helmholtz API. Status Code: {StatusCode}. Reason: {ReasonPhrase}", response.StatusCode, response.ReasonPhrase);
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.UNKNOWN, e.Message);
|
||||
await this.SendTimeoutError("loading the available models");
|
||||
LOGGER.LogError(e, "Timed out while loading models from Helmholtz provider '{ProviderInstanceName}'.", this.InstanceName);
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -65,9 +65,9 @@ public sealed class ProviderHuggingFace : BaseProvider
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
|
||||
@ -68,7 +68,7 @@ public interface IProvider
|
||||
/// <param name="settingsManager">The settings manager instance to use.</param>
|
||||
/// <param name="token">The cancellation token.</param>
|
||||
/// <returns>>The transcription result.</returns>
|
||||
public Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default);
|
||||
public Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Embed a text file.
|
||||
|
||||
@ -67,7 +67,7 @@ public sealed class ProviderMistral() : BaseProvider(LLMProviders.MISTRAL, "http
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<string> TranscribeAudioAsync(Provider.Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override async Task<TranscriptionResult> TranscribeAudioAsync(Provider.Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER);
|
||||
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
||||
|
||||
@ -5,6 +5,8 @@ public enum ModelLoadFailureReason
|
||||
NONE,
|
||||
INVALID_OR_MISSING_API_KEY,
|
||||
AUTHENTICATION_OR_PERMISSION_ERROR,
|
||||
INSUFFICIENT_QUOTA,
|
||||
TOO_MANY_REQUESTS,
|
||||
PROVIDER_UNAVAILABLE,
|
||||
INVALID_RESPONSE,
|
||||
UNKNOWN,
|
||||
|
||||
@ -10,6 +10,8 @@ public static class ModelLoadFailureReasonExtensions
|
||||
{
|
||||
ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY => string.Format(TB("We could not load models from '{0}'. The API key is probably missing, invalid, or expired."), providerName),
|
||||
ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => string.Format(TB("We could not load models from '{0}'. The account or API key does not have the required permissions."), providerName),
|
||||
ModelLoadFailureReason.INSUFFICIENT_QUOTA => string.Format(TB("We could not load models from '{0}' because the account appears to have no API credits left."), providerName),
|
||||
ModelLoadFailureReason.TOO_MANY_REQUESTS => string.Format(TB("We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again."), providerName),
|
||||
ModelLoadFailureReason.PROVIDER_UNAVAILABLE => string.Format(TB("We could not load models from '{0}' because the provider is currently unavailable or could not be reached."), providerName),
|
||||
ModelLoadFailureReason.INVALID_RESPONSE => string.Format(TB("We could not load models from '{0}' because the provider returned an unexpected response."), providerName),
|
||||
ModelLoadFailureReason.UNKNOWN => string.Format(TB("We could not load models from '{0}' due to an unknown error."), providerName),
|
||||
|
||||
@ -43,7 +43,7 @@ public class NoProvider : IProvider
|
||||
yield break;
|
||||
}
|
||||
|
||||
public Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) => Task.FromResult(string.Empty);
|
||||
public Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default) => Task.FromResult(TranscriptionResult.Failure());
|
||||
|
||||
public Task<IReadOnlyList<IReadOnlyList<float>>> EmbedTextAsync(Model embeddingModel, SettingsManager settingsManager, CancellationToken token = default, params List<string> texts) => Task.FromResult<IReadOnlyList<IReadOnlyList<float>>>([]);
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
@ -5,6 +6,7 @@ using System.Text.Json;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Provider.OpenAI;
|
||||
|
||||
@ -15,6 +17,8 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
|
||||
{
|
||||
private static readonly ILogger<ProviderOpenAI> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ProviderOpenAI>();
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderOpenAI).Namespace, nameof(ProviderOpenAI));
|
||||
|
||||
#region Implementation of IProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -26,6 +30,28 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
|
||||
/// <inheritdoc />
|
||||
public override bool HasModelLoadingCapability => true;
|
||||
|
||||
protected override ProviderRequestFailureReason ClassifyProviderRequestFailure(HttpStatusCode statusCode, string responseBody)
|
||||
{
|
||||
if (statusCode is HttpStatusCode.TooManyRequests && HasInsufficientQuotaError(responseBody))
|
||||
return ProviderRequestFailureReason.INSUFFICIENT_QUOTA;
|
||||
|
||||
return base.ClassifyProviderRequestFailure(statusCode, responseBody);
|
||||
}
|
||||
|
||||
protected override ProviderRequestFailureReason ClassifyProviderRequestFailure(string? errorCode, string? errorType, string? errorMessage, string responseBody)
|
||||
{
|
||||
if (IsInsufficientQuota(errorCode) || IsInsufficientQuota(errorType) || HasInsufficientQuotaError(responseBody))
|
||||
return ProviderRequestFailureReason.INSUFFICIENT_QUOTA;
|
||||
|
||||
return base.ClassifyProviderRequestFailure(errorCode, errorType, errorMessage, responseBody);
|
||||
}
|
||||
|
||||
protected override string GetProviderRequestFailureUserMessage(ProviderRequestFailureReason failureReason) => failureReason switch
|
||||
{
|
||||
ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("It looks like you do not have any API credits left with OpenAI. Please add credits to your account and try again."),
|
||||
_ => base.GetProviderRequestFailureUserMessage(failureReason),
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async IAsyncEnumerable<ContentStreamChunk> StreamChatCompletion(Model chatModel, ChatThread chatThread, SettingsManager settingsManager, [EnumeratorCancellation] CancellationToken token = default)
|
||||
{
|
||||
@ -222,7 +248,7 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override async Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER);
|
||||
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, token: token);
|
||||
@ -289,4 +315,59 @@ public sealed class ProviderOpenAI() : BaseProvider(LLMProviders.OPEN_AI, "https
|
||||
token,
|
||||
apiKeyProvisional);
|
||||
}
|
||||
|
||||
private static bool HasInsufficientQuotaError(string responseBody)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(responseBody))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(responseBody);
|
||||
return HasInsufficientQuotaError(document.RootElement);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasInsufficientQuotaError(JsonElement element)
|
||||
{
|
||||
switch (element.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
if (HasJsonStringValue(element, "type", "insufficient_quota") ||
|
||||
HasJsonStringValue(element, "code", "insufficient_quota"))
|
||||
return true;
|
||||
|
||||
foreach (var property in element.EnumerateObject())
|
||||
if (HasInsufficientQuotaError(property.Value))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
|
||||
case JsonValueKind.Array:
|
||||
foreach (var item in element.EnumerateArray())
|
||||
if (HasInsufficientQuotaError(item))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsInsufficientQuota(string? value)
|
||||
{
|
||||
return value is not null && value.Equals("insufficient_quota", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool HasJsonStringValue(JsonElement element, string propertyName, string expectedValue)
|
||||
{
|
||||
return element.TryGetProperty(propertyName, out var propertyElement) &&
|
||||
propertyElement.ValueKind is JsonValueKind.String &&
|
||||
string.Equals(propertyElement.GetString(), expectedValue, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@ -71,9 +71,9 @@ public sealed class ProviderOpenRouter() : BaseProvider(LLMProviders.OPEN_ROUTER
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
|
||||
@ -68,9 +68,9 @@ public sealed class ProviderPerplexity() : BaseProvider(LLMProviders.PERPLEXITY,
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
|
||||
25
app/MindWork AI Studio/Provider/ProviderRequestException.cs
Normal file
25
app/MindWork AI Studio/Provider/ProviderRequestException.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System.Net;
|
||||
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
public sealed class ProviderRequestException(
|
||||
ProviderRequestFailureReason failureReason,
|
||||
string userMessage,
|
||||
HttpStatusCode? statusCode = null,
|
||||
string reasonPhrase = "",
|
||||
string responseBody = "") : Exception(userMessage)
|
||||
{
|
||||
public ProviderRequestException() : this(ProviderRequestFailureReason.NONE, string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public ProviderRequestFailureReason FailureReason { get; } = failureReason;
|
||||
|
||||
public string UserMessage { get; } = userMessage;
|
||||
|
||||
public HttpStatusCode? StatusCode { get; } = statusCode;
|
||||
|
||||
public string ReasonPhrase { get; } = reasonPhrase;
|
||||
|
||||
public string ResponseBody { get; } = responseBody;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
public enum ProviderRequestFailureReason
|
||||
{
|
||||
NONE,
|
||||
INSUFFICIENT_QUOTA,
|
||||
TOO_MANY_REQUESTS,
|
||||
}
|
||||
@ -73,7 +73,7 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<string> TranscribeAudioAsync(Provider.Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override async Task<TranscriptionResult> TranscribeAudioAsync(Provider.Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
var requestedSecret = await RUST_SERVICE.GetAPIKey(this, SecretStoreType.TRANSCRIPTION_PROVIDER, isTrying: true);
|
||||
return await this.PerformStandardTranscriptionRequest(requestedSecret, transcriptionModel, audioFilePath, host, token);
|
||||
@ -172,19 +172,32 @@ public sealed class ProviderSelfHosted(Host host, string hostname) : BaseProvide
|
||||
private async Task<ModelLoadResult> LoadModels(SecretStoreType storeType, string[] ignorePhrases, string[] filterPhrases, CancellationToken token, string? apiKeyProvisional = null)
|
||||
{
|
||||
var secretKey = await this.GetModelLoadingSecretKey(storeType, apiKeyProvisional, true);
|
||||
|
||||
using var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
if(secretKey is not null)
|
||||
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var lmStudioResponse = await this.HttpClient.SendAsync(lmStudioRequest, token);
|
||||
if(!lmStudioResponse.IsSuccessStatusCode)
|
||||
return FailedModelLoadResult(GetDefaultModelLoadFailureReason(lmStudioResponse), $"Status={(int)lmStudioResponse.StatusCode} {lmStudioResponse.ReasonPhrase}");
|
||||
|
||||
var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return SuccessfulModelLoadResult(lmStudioModelResponse.Data.
|
||||
Where(model => !ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCulture)) &&
|
||||
filterPhrases.All( filter => model.Id.Contains(filter, StringComparison.InvariantCulture)))
|
||||
.Select(n => new Provider.Model(n.Id, null)));
|
||||
try
|
||||
{
|
||||
using var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models");
|
||||
if(secretKey is not null)
|
||||
lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", secretKey);
|
||||
|
||||
using var lmStudioResponse = await this.HttpClient.SendAsync(lmStudioRequest, token);
|
||||
if(!lmStudioResponse.IsSuccessStatusCode)
|
||||
{
|
||||
var responseBody = await lmStudioResponse.Content.ReadAsStringAsync(token);
|
||||
LOGGER.LogError("Model loading request failed with status code {ResponseStatusCode} (message = '{ResponseReasonPhrase}', error body = '{ErrorBody}').", lmStudioResponse.StatusCode, lmStudioResponse.ReasonPhrase, responseBody);
|
||||
return FailedModelLoadResult(this.GetModelLoadFailureReason(lmStudioResponse, responseBody), $"Status={(int)lmStudioResponse.StatusCode} {lmStudioResponse.ReasonPhrase}; Body='{responseBody}'");
|
||||
}
|
||||
|
||||
var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync<ModelsResponse>(token);
|
||||
return SuccessfulModelLoadResult(lmStudioModelResponse.Data.
|
||||
Where(model => !ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCulture)) &&
|
||||
filterPhrases.All( filter => model.Id.Contains(filter, StringComparison.InvariantCulture)))
|
||||
.Select(n => new Provider.Model(n.Id, null)));
|
||||
}
|
||||
catch (Exception e) when (this.IsTimeoutException(e, token))
|
||||
{
|
||||
await this.SendTimeoutError("loading the available models");
|
||||
LOGGER.LogError(e, "Timed out while loading models from self-hosted provider '{ProviderInstanceName}'.", this.InstanceName);
|
||||
return FailedModelLoadResult(ModelLoadFailureReason.PROVIDER_UNAVAILABLE, e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
app/MindWork AI Studio/Provider/TranscriptionResult.cs
Normal file
8
app/MindWork AI Studio/Provider/TranscriptionResult.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
public sealed record TranscriptionResult(bool Success, string Text, string ErrorMessage = "")
|
||||
{
|
||||
public static TranscriptionResult FromText(string text) => new(true, text);
|
||||
|
||||
public static TranscriptionResult Failure(string errorMessage = "") => new(false, string.Empty, errorMessage);
|
||||
}
|
||||
@ -61,9 +61,9 @@ public sealed class ProviderX() : BaseProvider(LLMProviders.X, "https://api.x.ai
|
||||
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<string> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
public override Task<TranscriptionResult> TranscribeAudioAsync(Model transcriptionModel, string audioFilePath, SettingsManager settingsManager, CancellationToken token = default)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
return Task.FromResult(TranscriptionResult.Failure());
|
||||
}
|
||||
|
||||
/// <inhertidoc />
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
using System.Text;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Lua;
|
||||
using SharedTools;
|
||||
|
||||
using LuaTable = Lua.LuaTable;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
@ -17,6 +21,8 @@ public record ChatTemplate(
|
||||
bool IsEnterpriseConfiguration = false,
|
||||
Guid EnterpriseConfigurationPluginId = default) : ConfigurationBaseObject
|
||||
{
|
||||
private const string ATTACHMENTS_DIRECTORY = "attachments";
|
||||
|
||||
public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], [], false)
|
||||
{
|
||||
}
|
||||
@ -73,8 +79,8 @@ public record ChatTemplate(
|
||||
|
||||
return this.SystemPrompt;
|
||||
}
|
||||
|
||||
public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject template)
|
||||
|
||||
public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject template)
|
||||
{
|
||||
template = NO_CHAT_TEMPLATE;
|
||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
||||
@ -103,7 +109,7 @@ public record ChatTemplate(
|
||||
if (table.TryGetValue("AllowProfileUsage", out var allowProfileValue) && allowProfileValue.TryRead<bool>(out var allow))
|
||||
allowProfileUsage = allow;
|
||||
|
||||
var fileAttachments = ParseFileAttachments(idx, table);
|
||||
var fileAttachments = ParseFileAttachments(idx, table, pluginPath);
|
||||
|
||||
template = new ChatTemplate
|
||||
{
|
||||
@ -169,7 +175,7 @@ public record ChatTemplate(
|
||||
return exampleConversation;
|
||||
}
|
||||
|
||||
private static List<FileAttachment> ParseFileAttachments(int idx, LuaTable table)
|
||||
private static List<FileAttachment> ParseFileAttachments(int idx, LuaTable table, string pluginPath)
|
||||
{
|
||||
var fileAttachments = new List<FileAttachment>();
|
||||
if (!table.TryGetValue("FileAttachments", out var fileAttValue) || !fileAttValue.TryRead<LuaTable>(out var fileAttTable))
|
||||
@ -185,9 +191,227 @@ public record ChatTemplate(
|
||||
continue;
|
||||
}
|
||||
|
||||
fileAttachments.Add(FileAttachment.FromPath(filePath));
|
||||
if (TryResolveFileAttachmentPath(idx, attachmentNum, filePath, pluginPath, out var resolvedFilePath))
|
||||
fileAttachments.Add(FileAttachment.FromPath(resolvedFilePath));
|
||||
}
|
||||
|
||||
return fileAttachments;
|
||||
}
|
||||
|
||||
private static bool TryResolveFileAttachmentPath(int idx, int attachmentNum, string filePath, string pluginPath, out string resolvedFilePath)
|
||||
{
|
||||
resolvedFilePath = filePath;
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
LOGGER.LogWarning("The FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} is empty.", attachmentNum, idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Path.IsPathFullyQualified(filePath))
|
||||
return true;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pluginPath))
|
||||
{
|
||||
LOGGER.LogWarning("The relative FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} cannot be resolved because the plugin path is unknown.", attachmentNum, idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
var pluginRoot = Path.GetFullPath(pluginPath);
|
||||
var relativePath = filePath
|
||||
.Replace('/', Path.DirectorySeparatorChar)
|
||||
.Replace('\\', Path.DirectorySeparatorChar);
|
||||
|
||||
if (relativePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Any(segment => segment == ".."))
|
||||
{
|
||||
LOGGER.LogWarning("The relative FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} contains '..' path segments and will be ignored.", attachmentNum, idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
var combinedPath = Path.GetFullPath(Path.Combine(pluginRoot, relativePath));
|
||||
var pluginRootWithSeparator = pluginRoot.EndsWith(Path.DirectorySeparatorChar)
|
||||
? pluginRoot
|
||||
: pluginRoot + Path.DirectorySeparatorChar;
|
||||
var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
|
||||
if (!combinedPath.StartsWith(pluginRootWithSeparator, comparison))
|
||||
{
|
||||
LOGGER.LogWarning("The relative FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} points outside of the plugin folder and will be ignored.", attachmentNum, idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
resolvedFilePath = combinedPath;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryExportAsConfigurationSection(out string luaCode, out string issue) => this.TryExportAsConfigurationSection(null, Guid.NewGuid().ToString(), out luaCode, out issue);
|
||||
|
||||
private bool TryExportAsConfigurationSection(IReadOnlyList<string>? fileAttachmentPaths, string exportId, out string luaCode, out string issue)
|
||||
{
|
||||
luaCode = string.Empty;
|
||||
issue = string.Empty;
|
||||
if (!this.TryBuildExampleConversationLua(out var exampleConversationLua, out issue))
|
||||
return false;
|
||||
|
||||
return this.TryExportAsConfigurationSection(fileAttachmentPaths, exportId, exampleConversationLua, out luaCode, out issue);
|
||||
}
|
||||
|
||||
private bool TryExportAsConfigurationSection(IReadOnlyList<string>? fileAttachmentPaths, string exportId, string exampleConversationLua, out string luaCode, out string issue)
|
||||
{
|
||||
issue = string.Empty;
|
||||
var fileAttachmentsLua = this.BuildFileAttachmentsLua(fileAttachmentPaths);
|
||||
luaCode = $$"""
|
||||
CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = {
|
||||
["Id"] = "{{LuaTools.EscapeLuaString(exportId)}}",
|
||||
["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}},
|
||||
["SystemPrompt"] = {{LuaTools.ToLuaStringLiteral(this.SystemPrompt)}},
|
||||
["PredefinedUserPrompt"] = {{LuaTools.ToLuaStringLiteral(this.PredefinedUserPrompt)}},
|
||||
["AllowProfileUsage"] = {{this.AllowProfileUsage.ToString().ToLowerInvariant()}},
|
||||
["FileAttachments"] = {{fileAttachmentsLua}},
|
||||
["ExampleConversation"] = {{exampleConversationLua}},
|
||||
}
|
||||
""";
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryExportAsConfigurationSectionWithPackagedAttachments(string pluginDirectory, out string luaCode, out string issue)
|
||||
{
|
||||
luaCode = string.Empty;
|
||||
issue = string.Empty;
|
||||
var exportId = Guid.NewGuid().ToString();
|
||||
|
||||
if (!this.TryBuildExampleConversationLua(out var exampleConversationLua, out issue))
|
||||
return false;
|
||||
|
||||
if (this.FileAttachments.Count == 0)
|
||||
return this.TryExportAsConfigurationSection(null, exportId, exampleConversationLua, out luaCode, out issue);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pluginDirectory) || !File.Exists(Path.Combine(pluginDirectory, "plugin.lua")))
|
||||
{
|
||||
issue = TB("Please select a valid configuration plugin folder. The folder must contain a plugin.lua file.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var sourcePaths = new List<string>();
|
||||
foreach (var attachment in this.FileAttachments)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(attachment.FilePath) || !File.Exists(attachment.FilePath))
|
||||
{
|
||||
issue = string.Format(TB("Cannot package the attachment '{0}' because the file does not exist."), attachment.FileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
sourcePaths.Add(attachment.FilePath);
|
||||
}
|
||||
|
||||
var targetDirectory = Path.Combine(pluginDirectory, ATTACHMENTS_DIRECTORY, exportId);
|
||||
var relativeAttachmentPaths = new List<string>();
|
||||
var usedFileNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
foreach (var sourcePath in sourcePaths)
|
||||
{
|
||||
var targetFileName = CreateUniqueAttachmentFileName(sourcePath, usedFileNames);
|
||||
var targetPath = Path.Combine(targetDirectory, targetFileName);
|
||||
File.Copy(sourcePath, targetPath, overwrite: false);
|
||||
relativeAttachmentPaths.Add($"{ATTACHMENTS_DIRECTORY}/{exportId}/{targetFileName}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(targetDirectory))
|
||||
Directory.Delete(targetDirectory, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep the original packaging error as the user-facing issue.
|
||||
}
|
||||
|
||||
issue = string.Format(TB("Cannot package the chat template attachments. The issue was: {0}"), e.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.TryExportAsConfigurationSection(relativeAttachmentPaths, exportId, exampleConversationLua, out luaCode, out issue);
|
||||
}
|
||||
|
||||
private bool TryBuildExampleConversationLua(out string luaTable, out string issue)
|
||||
{
|
||||
luaTable = "{}";
|
||||
issue = string.Empty;
|
||||
if (this.ExampleConversation.Count == 0)
|
||||
return true;
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("{");
|
||||
for (var i = 0; i < this.ExampleConversation.Count; i++)
|
||||
{
|
||||
var block = this.ExampleConversation[i];
|
||||
if (block.Role is not ChatRole.USER and not ChatRole.AI)
|
||||
{
|
||||
issue = string.Format(TB("Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins."), i + 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (block.Content is not ContentText textContent)
|
||||
{
|
||||
issue = string.Format(TB("Cannot export this chat template because example message {0} is not a text message."), i + 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(textContent.Text))
|
||||
{
|
||||
issue = string.Format(TB("Cannot export this chat template because example message {0} is empty."), i + 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine($" [\"Role\"] = \"{block.Role}\",");
|
||||
builder.AppendLine($" [\"Content\"] = {LuaTools.ToLuaStringLiteral(textContent.Text)},");
|
||||
builder.AppendLine(" },");
|
||||
}
|
||||
|
||||
builder.Append(" }");
|
||||
luaTable = builder.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
private string BuildFileAttachmentsLua(IReadOnlyList<string>? fileAttachmentPaths)
|
||||
{
|
||||
var paths = fileAttachmentPaths ?? this.FileAttachments.Select(attachment => attachment.FilePath).ToList();
|
||||
if (paths.Count == 0)
|
||||
return "{}";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("{");
|
||||
foreach (var path in paths)
|
||||
builder.AppendLine($" \"{LuaTools.EscapeLuaString(path)}\",");
|
||||
|
||||
builder.Append(" }");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string CreateUniqueAttachmentFileName(string sourcePath, HashSet<string> usedFileNames)
|
||||
{
|
||||
var fileName = SanitizeFileName(Path.GetFileName(sourcePath));
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
fileName = "attachment";
|
||||
|
||||
var extension = Path.GetExtension(fileName);
|
||||
var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
|
||||
var candidate = fileName;
|
||||
var counter = 2;
|
||||
while (!usedFileNames.Add(candidate))
|
||||
candidate = $"{nameWithoutExtension}-{counter++}{extension}";
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static string SanitizeFileName(string fileName)
|
||||
{
|
||||
foreach (var invalidChar in Path.GetInvalidFileNameChars())
|
||||
fileName = fileName.Replace(invalidChar, '_');
|
||||
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
@ -94,6 +94,11 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
||||
/// </summary>
|
||||
public string ShortcutVoiceRecording { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShortcutVoiceRecording, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP timeout in seconds for external HTTP clients.
|
||||
/// </summary>
|
||||
public int HttpClientTimeoutSeconds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.HttpClientTimeoutSeconds, ExternalHttpClientTimeout.DEFAULT_HTTP_CLIENT_TIMEOUT_SECONDS);
|
||||
|
||||
/// <summary>
|
||||
/// Should the user be allowed to add providers?
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public enum DataSourceERIUsernamePasswordMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The user manages the username and password locally.
|
||||
/// </summary>
|
||||
USER_MANAGED,
|
||||
|
||||
/// <summary>
|
||||
/// The username and password are shared by all users and provided by configuration.
|
||||
/// </summary>
|
||||
SHARED_USERNAME_AND_PASSWORD,
|
||||
|
||||
/// <summary>
|
||||
/// The username is read from the operating system, and the password is shared by all users.
|
||||
/// </summary>
|
||||
OS_USERNAME_SHARED_PASSWORD,
|
||||
}
|
||||
@ -4,11 +4,15 @@ using AIStudio.Assistants.ERI;
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Tools.ERIClient;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.RAG;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
using ChatThread = AIStudio.Chat.ChatThread;
|
||||
using ContentType = AIStudio.Tools.ERIClient.DataModel.ContentType;
|
||||
using LuaTable = Lua.LuaTable;
|
||||
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
@ -17,6 +21,8 @@ namespace AIStudio.Settings.DataModel;
|
||||
/// </summary>
|
||||
public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
{
|
||||
private static readonly ILogger<DataSourceERI_V1> LOGGER = Program.LOGGER_FACTORY.CreateLogger<DataSourceERI_V1>();
|
||||
|
||||
public DataSourceERI_V1()
|
||||
{
|
||||
}
|
||||
@ -45,8 +51,17 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
/// <inheritdoc />
|
||||
public string Username { get; init; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DataSourceERIUsernamePasswordMode UsernamePasswordMode { get; init; } = DataSourceERIUsernamePasswordMode.USER_MANAGED;
|
||||
|
||||
/// <inheritdoc />
|
||||
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnterpriseConfiguration { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ERIVersion Version { get; init; } = ERIVersion.V1;
|
||||
@ -82,7 +97,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
|
||||
Thread = await thread.ToERIChatThread(token),
|
||||
MaxMatches = this.MaxMatches,
|
||||
RetrievalProcessId = string.IsNullOrWhiteSpace(this.SelectedRetrievalId) ? null : this.SelectedRetrievalId,
|
||||
RetrievalProcessId = this.SelectedRetrievalId,
|
||||
Parameters = null, // The ERI server selects useful default parameters
|
||||
};
|
||||
|
||||
@ -139,4 +154,240 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource
|
||||
logger.LogWarning($"Was not able to authenticate with the ERI data source '{this.Name}'. Message: {authResponse.Message}");
|
||||
return [];
|
||||
}
|
||||
|
||||
public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPluginId, out DataSourceERI_V1 dataSource)
|
||||
{
|
||||
dataSource = default;
|
||||
if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name) || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid name. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!table.TryGetValue("Type", out var typeValue) || !typeValue.TryRead<string>(out var typeText) || !Enum.TryParse<DataSourceType>(typeText, true, out var type) || type is not DataSourceType.ERI_V1)
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a supported data source type. Only ERI_V1 is supported. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname) || string.IsNullOrWhiteSpace(hostname))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!table.TryGetValue("Port", out var portValue) || !portValue.TryRead<int>(out var port) || port is < 1 or > 65535)
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid port. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!table.TryGetValue("AuthMethod", out var authMethodValue) || !authMethodValue.TryRead<string>(out var authMethodText) || !Enum.TryParse<AuthMethod>(authMethodText, true, out var authMethod))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid auth method. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!table.TryGetValue("SecurityPolicy", out var securityPolicyValue) || !securityPolicyValue.TryRead<string>(out var securityPolicyText) || !Enum.TryParse<DataSourceSecurity>(securityPolicyText, true, out var securityPolicy))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid security policy. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (securityPolicy is DataSourceSecurity.NOT_SPECIFIED)
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} must specify a security policy. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!table.TryGetValue("SelectedRetrievalId", out var selectedRetrievalIdValue) || !selectedRetrievalIdValue.TryRead<string>(out var selectedRetrievalId) || string.IsNullOrWhiteSpace(selectedRetrievalId))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} must specify a selected retrieval ID. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!table.TryGetValue("MaxMatches", out var maxMatchesValue) || !maxMatchesValue.TryRead<int>(out var maxMatches) || maxMatches is < 1 or > ushort.MaxValue)
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid maximum number of matches. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
var username = string.Empty;
|
||||
var usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED;
|
||||
if (table.TryGetValue("UsernamePasswordMode", out var usernamePasswordModeValue) && usernamePasswordModeValue.TryRead<string>(out var usernamePasswordModeText))
|
||||
{
|
||||
if (!Enum.TryParse(usernamePasswordModeText, true, out usernamePasswordMode))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid username/password mode. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.USER_MANAGED)
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} uses the user-managed username/password mode. This mode is not allowed in configuration plugins. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (authMethod is AuthMethod.USERNAME_PASSWORD)
|
||||
{
|
||||
if (!table.TryGetValue("UsernamePasswordMode", out _) || usernamePasswordMode is DataSourceERIUsernamePasswordMode.USER_MANAGED)
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} must specify an organization-managed username/password mode. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD &&
|
||||
(!table.TryGetValue("Username", out var usernameValue) || !usernameValue.TryRead<string>(out username) || string.IsNullOrWhiteSpace(username)))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} must specify a username. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
dataSource = new DataSourceERI_V1
|
||||
{
|
||||
Num = 0,
|
||||
Id = id.ToString(),
|
||||
Name = name,
|
||||
Type = DataSourceType.ERI_V1,
|
||||
Hostname = CleanHostname(hostname),
|
||||
Port = port,
|
||||
AuthMethod = authMethod,
|
||||
Username = username,
|
||||
UsernamePasswordMode = usernamePasswordMode,
|
||||
SecurityPolicy = securityPolicy,
|
||||
Version = ERIVersion.V1,
|
||||
SelectedRetrievalId = selectedRetrievalId,
|
||||
MaxMatches = (ushort)maxMatches,
|
||||
IsEnterpriseConfiguration = true,
|
||||
EnterpriseConfigurationPluginId = configPluginId,
|
||||
};
|
||||
|
||||
return TryQueueEnterpriseSecret(idx, table, configPluginId, dataSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports the ERI v1 data source configuration as a Lua configuration section.
|
||||
/// </summary>
|
||||
/// <param name="encryptedSecret">Optional encrypted token or password to include in the export.</param>
|
||||
/// <param name="usernamePasswordMode">The organization-managed username/password mode to export.</param>
|
||||
/// <returns>A Lua configuration section string.</returns>
|
||||
public string ExportAsConfigurationSection(string? encryptedSecret = null, DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED)
|
||||
{
|
||||
var secretLine = string.Empty;
|
||||
var usernamePasswordModeLine = string.Empty;
|
||||
var usernameLine = string.Empty;
|
||||
|
||||
switch (this.AuthMethod)
|
||||
{
|
||||
case AuthMethod.TOKEN:
|
||||
secretLine = CreateSecretLine("Token", encryptedSecret);
|
||||
break;
|
||||
|
||||
case AuthMethod.USERNAME_PASSWORD:
|
||||
if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.USER_MANAGED)
|
||||
usernamePasswordMode = DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD;
|
||||
|
||||
usernamePasswordModeLine = $"""
|
||||
["UsernamePasswordMode"] = "{usernamePasswordMode}",
|
||||
""";
|
||||
|
||||
if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD)
|
||||
{
|
||||
var username = string.IsNullOrWhiteSpace(this.Username) ? "<shared username>" : this.Username;
|
||||
usernameLine = $"""
|
||||
["Username"] = "{LuaTools.EscapeLuaString(username)}",
|
||||
""";
|
||||
}
|
||||
|
||||
secretLine = CreateSecretLine("Password", encryptedSecret);
|
||||
break;
|
||||
}
|
||||
|
||||
return $$"""
|
||||
CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = {
|
||||
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
||||
["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}",
|
||||
["Type"] = "ERI_V1",
|
||||
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",
|
||||
["Port"] = {{this.Port}},
|
||||
["AuthMethod"] = "{{this.AuthMethod}}",
|
||||
{{usernamePasswordModeLine}}
|
||||
{{usernameLine}}
|
||||
{{secretLine}}
|
||||
["SecurityPolicy"] = "{{this.SecurityPolicy}}",
|
||||
["SelectedRetrievalId"] = "{{LuaTools.EscapeLuaString(this.SelectedRetrievalId)}}",
|
||||
["MaxMatches"] = {{this.MaxMatches}},
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
private static bool TryQueueEnterpriseSecret(int idx, LuaTable table, Guid configPluginId, DataSourceERI_V1 dataSource)
|
||||
{
|
||||
var secretFieldName = dataSource.AuthMethod switch
|
||||
{
|
||||
AuthMethod.TOKEN => "Token",
|
||||
AuthMethod.USERNAME_PASSWORD => "Password",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(secretFieldName))
|
||||
return true;
|
||||
|
||||
if (!table.TryGetValue(secretFieldName, out var secretValue) || !secretValue.TryRead<string>(out var encryptedSecret) || string.IsNullOrWhiteSpace(encryptedSecret))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} does not contain a valid encrypted {secretFieldName}. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnterpriseEncryption.IsEncrypted(encryptedSecret))
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} contains a plaintext {secretFieldName}. Only encrypted secrets (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
var encryption = PluginFactory.EnterpriseEncryption;
|
||||
if (encryption?.IsAvailable != true)
|
||||
{
|
||||
LOGGER.LogWarning($"The configured data source {idx} contains an encrypted {secretFieldName}, but no encryption secret is configured. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!encryption.TryDecrypt(encryptedSecret, out var decryptedSecret))
|
||||
{
|
||||
LOGGER.LogWarning($"Failed to decrypt the {secretFieldName} for data source {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})");
|
||||
return false;
|
||||
}
|
||||
|
||||
PendingEnterpriseSecrets.Add(new(
|
||||
$"{ISecretId.ENTERPRISE_KEY_PREFIX}::{dataSource.Id}",
|
||||
dataSource.Name,
|
||||
decryptedSecret,
|
||||
SecretStoreType.DATA_SOURCE));
|
||||
LOGGER.LogDebug($"Successfully decrypted the {secretFieldName} for data source {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string CreateSecretLine(string fieldName, string? encryptedSecret)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(encryptedSecret))
|
||||
return string.Empty;
|
||||
|
||||
return $"""
|
||||
["{fieldName}"] = "{LuaTools.EscapeLuaString(encryptedSecret)}",
|
||||
""";
|
||||
}
|
||||
|
||||
private static string CleanHostname(string hostname)
|
||||
{
|
||||
var cleanedHostname = hostname.Trim();
|
||||
return cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname;
|
||||
}
|
||||
}
|
||||
@ -35,6 +35,12 @@ public readonly record struct DataSourceLocalDirectory : IInternalDataSource
|
||||
|
||||
/// <inheritdoc />
|
||||
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnterpriseConfiguration { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ushort MaxMatches { get; init; } = 10;
|
||||
|
||||
@ -35,6 +35,12 @@ public readonly record struct DataSourceLocalFile : IInternalDataSource
|
||||
|
||||
/// <inheritdoc />
|
||||
public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsEnterpriseConfiguration { get; init; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ushort MaxMatches { get; init; } = 10;
|
||||
|
||||
@ -14,7 +14,7 @@ public static class PreviewFeaturesExtensions
|
||||
PreviewFeatures.PRE_PLUGINS_2025 => TB("Plugins: Preview of our plugin system where you can extend the functionality of the app"),
|
||||
PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"),
|
||||
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"),
|
||||
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text"),
|
||||
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"),
|
||||
|
||||
_ => TB("Unknown preview feature")
|
||||
};
|
||||
@ -33,6 +33,7 @@ public static class PreviewFeaturesExtensions
|
||||
PreviewFeatures.PRE_READ_PDF_2025 => true,
|
||||
PreviewFeatures.PRE_PLUGINS_2025 => true,
|
||||
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true,
|
||||
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => true,
|
||||
|
||||
_ => false
|
||||
};
|
||||
|
||||
@ -12,7 +12,6 @@ public static class PreviewVisibilityExtensions
|
||||
if (visibility >= PreviewVisibility.BETA)
|
||||
{
|
||||
features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025);
|
||||
features.Add(PreviewFeatures.PRE_SPEECH_TO_TEXT_2026);
|
||||
}
|
||||
|
||||
if (visibility >= PreviewVisibility.ALPHA)
|
||||
|
||||
@ -3,9 +3,10 @@ using System.Text.Json.Serialization;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Lua;
|
||||
using SharedTools;
|
||||
|
||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||
using LuaTable = Lua.LuaTable;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ using System.Text.Json.Serialization;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.RAG;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
@ -13,23 +14,8 @@ namespace AIStudio.Settings;
|
||||
[JsonDerivedType(typeof(DataSourceLocalDirectory), nameof(DataSourceType.LOCAL_DIRECTORY))]
|
||||
[JsonDerivedType(typeof(DataSourceLocalFile), nameof(DataSourceType.LOCAL_FILE))]
|
||||
[JsonDerivedType(typeof(DataSourceERI_V1), nameof(DataSourceType.ERI_V1))]
|
||||
public interface IDataSource
|
||||
public interface IDataSource : IConfigurationObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of the data source.
|
||||
/// </summary>
|
||||
public uint Num { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The unique identifier of the data source.
|
||||
/// </summary>
|
||||
public string Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the data source.
|
||||
/// </summary>
|
||||
public string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Which type of data source is this?
|
||||
/// </summary>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using AIStudio.Assistants.ERI;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.ERIClient.DataModel;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
@ -24,6 +25,11 @@ public interface IERIDataSource : IExternalDataSource
|
||||
/// The username to use for authentication, when the auth. method is USERNAME_PASSWORD.
|
||||
/// </summary>
|
||||
public string Username { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// How username/password authentication should obtain the username.
|
||||
/// </summary>
|
||||
public DataSourceERIUsernamePasswordMode UsernamePasswordMode { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The ERI specification to use.
|
||||
|
||||
@ -7,7 +7,7 @@ public interface IExternalDataSource : IDataSource, ISecretId
|
||||
#region Implementation of ISecretId
|
||||
|
||||
[JsonIgnore]
|
||||
string ISecretId.SecretId => this.Id;
|
||||
string ISecretId.SecretId => this.IsEnterpriseConfiguration ? $"{ENTERPRISE_KEY_PREFIX}::{this.Id}" : this.Id;
|
||||
|
||||
[JsonIgnore]
|
||||
string ISecretId.SecretName => this.Name;
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using Lua;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
using LuaTable = Lua.LuaTable;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
@ -132,4 +135,20 @@ public record Profile(
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports the profile configuration as a Lua configuration section.
|
||||
/// </summary>
|
||||
/// <returns>A Lua configuration section string.</returns>
|
||||
public string ExportAsConfigurationSection()
|
||||
{
|
||||
return $$"""
|
||||
CONFIG["PROFILES"][#CONFIG["PROFILES"]+1] = {
|
||||
["Id"] = "{{Guid.NewGuid().ToString()}}",
|
||||
["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}},
|
||||
["NeedToKnow"] = {{LuaTools.ToLuaStringLiteral(this.NeedToKnow)}},
|
||||
["Actions"] = {{LuaTools.ToLuaStringLiteral(this.Actions)}},
|
||||
}
|
||||
""";
|
||||
}
|
||||
}
|
||||
@ -4,9 +4,10 @@ using AIStudio.Provider;
|
||||
using AIStudio.Provider.HuggingFace;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Lua;
|
||||
using SharedTools;
|
||||
|
||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||
using LuaTable = Lua.LuaTable;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
|
||||
@ -3,9 +3,10 @@ using System.Text.Json.Serialization;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Lua;
|
||||
using SharedTools;
|
||||
|
||||
using Host = AIStudio.Provider.SelfHosted.Host;
|
||||
using LuaTable = Lua.LuaTable;
|
||||
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
|
||||
7
app/MindWork AI Studio/Tools/AIJobs/AIJobKind.cs
Normal file
7
app/MindWork AI Studio/Tools/AIJobs/AIJobKind.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace AIStudio.Tools.AIJobs;
|
||||
|
||||
public enum AIJobKind
|
||||
{
|
||||
NONE,
|
||||
CHAT_GENERATION,
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Tools.AIJobs;
|
||||
|
||||
public enum AIJobSchedulingClass
|
||||
{
|
||||
NONE,
|
||||
TOP_LEVEL_USER_JOB,
|
||||
INTERNAL_DEPENDENCY,
|
||||
}
|
||||
458
app/MindWork AI Studio/Tools/AIJobs/AIJobService.cs
Normal file
458
app/MindWork AI Studio/Tools/AIJobs/AIJobService.cs
Normal file
@ -0,0 +1,458 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using AIStudio.Chat;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.RAG.RAGProcesses;
|
||||
|
||||
namespace AIStudio.Tools.AIJobs;
|
||||
|
||||
public sealed class AIJobService(
|
||||
SettingsManager settingsManager,
|
||||
MessageBus messageBus,
|
||||
ILogger<AIJobService> logger)
|
||||
{
|
||||
private sealed class AIJobState
|
||||
{
|
||||
public required CancellationTokenSource CancellationTokenSource { get; init; }
|
||||
|
||||
public required CancellationToken CancellationToken { get; init; }
|
||||
|
||||
public required ChatGenerationRequest ChatGenerationRequest { get; init; }
|
||||
|
||||
public required AIJobSnapshot Snapshot { get; set; }
|
||||
|
||||
public DateTimeOffset LastCheckpoint { get; set; }
|
||||
|
||||
public bool IsCompletionStarted { get; set; }
|
||||
|
||||
public readonly Lock SyncRoot = new();
|
||||
}
|
||||
|
||||
private static readonly TimeSpan STREAMING_EVENT_MIN_TIME = TimeSpan.FromSeconds(3);
|
||||
|
||||
private static readonly TimeSpan CHECKPOINT_MIN_TIME = TimeSpan.FromSeconds(3);
|
||||
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AIJobService).Namespace, nameof(AIJobService));
|
||||
|
||||
private readonly ConcurrentDictionary<Guid, AIJobState> jobs = new();
|
||||
private readonly ConcurrentDictionary<Guid, Guid> activeChatJobsByChatId = new();
|
||||
|
||||
public IReadOnlyCollection<AIJobSnapshot> GetSnapshots()
|
||||
{
|
||||
return this.jobs.Values
|
||||
.Select(job => job.Snapshot)
|
||||
.OrderByDescending(snapshot => snapshot.UpdatedAt)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public bool HasActiveJobs => this.jobs.Values.Any(job => job.Snapshot.IsActive);
|
||||
|
||||
public bool IsChatGenerationActive(Guid chatId)
|
||||
{
|
||||
if (!this.activeChatJobsByChatId.TryGetValue(chatId, out var jobId))
|
||||
return false;
|
||||
|
||||
return this.jobs.TryGetValue(jobId, out var job) && job.Snapshot.IsActive;
|
||||
}
|
||||
|
||||
public AIJobSnapshot? TryGetChatSnapshot(Guid chatId)
|
||||
{
|
||||
if (!this.activeChatJobsByChatId.TryGetValue(chatId, out var jobId))
|
||||
return this.jobs.Values
|
||||
.Select(job => job.Snapshot)
|
||||
.Where(snapshot => snapshot.Kind is AIJobKind.CHAT_GENERATION && snapshot.SubjectId == chatId)
|
||||
.MaxBy(snapshot => snapshot.UpdatedAt);
|
||||
|
||||
return this.jobs.TryGetValue(jobId, out var activeJob) ? activeJob.Snapshot : null;
|
||||
}
|
||||
|
||||
public ChatThread? TryGetLiveChatThread(Guid chatId)
|
||||
{
|
||||
if (!this.activeChatJobsByChatId.TryGetValue(chatId, out var jobId))
|
||||
return null;
|
||||
|
||||
return this.jobs.TryGetValue(jobId, out var job) ? job.ChatGenerationRequest.ChatThread : null;
|
||||
}
|
||||
|
||||
public async Task<AIJobSnapshot?> TryStartChatGenerationAsync(ChatGenerationRequest request)
|
||||
{
|
||||
if (this.activeChatJobsByChatId.TryGetValue(request.ChatThread.ChatId, out var existingJobId))
|
||||
return this.jobs.TryGetValue(existingJobId, out var existingJob) ? existingJob.Snapshot : null;
|
||||
|
||||
var jobId = Guid.NewGuid();
|
||||
var rootJobId = request.ParentJobId ?? jobId;
|
||||
var snapshot = new AIJobSnapshot
|
||||
{
|
||||
JobId = jobId,
|
||||
Kind = AIJobKind.CHAT_GENERATION,
|
||||
SubjectId = request.ChatThread.ChatId,
|
||||
ParentJobId = request.ParentJobId,
|
||||
RootJobId = rootJobId,
|
||||
Priority = request.Priority,
|
||||
IsForeground = request.IsForeground,
|
||||
SchedulingClass = AIJobSchedulingClass.TOP_LEVEL_USER_JOB,
|
||||
Status = AIJobStatus.WAITING_FOR_REMOTE,
|
||||
Title = request.ChatThread.Name,
|
||||
ProviderId = request.ProviderSettings.Id,
|
||||
ModelId = request.ProviderSettings.Model.Id,
|
||||
UpdatedAt = DateTimeOffset.Now,
|
||||
};
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var state = new AIJobState
|
||||
{
|
||||
CancellationTokenSource = cancellationTokenSource,
|
||||
CancellationToken = cancellationTokenSource.Token,
|
||||
ChatGenerationRequest = request,
|
||||
Snapshot = snapshot,
|
||||
LastCheckpoint = DateTimeOffset.MinValue,
|
||||
};
|
||||
|
||||
if (!this.activeChatJobsByChatId.TryAdd(request.ChatThread.ChatId, jobId))
|
||||
{
|
||||
state.CancellationTokenSource.Dispose();
|
||||
return this.TryGetChatSnapshot(request.ChatThread.ChatId);
|
||||
}
|
||||
|
||||
if (!this.jobs.TryAdd(jobId, state))
|
||||
{
|
||||
this.activeChatJobsByChatId.TryRemove(request.ChatThread.ChatId, out _);
|
||||
state.CancellationTokenSource.Dispose();
|
||||
return null;
|
||||
}
|
||||
|
||||
request.AIText.InitialRemoteWait = true;
|
||||
request.AIText.IsStreaming = false;
|
||||
await CheckpointChatAsync(state, force: true);
|
||||
await this.NotifyChangedAsync(state);
|
||||
|
||||
_ = Task.Factory.StartNew(async () => await this.RunChatGenerationAsync(state), TaskCreationOptions.LongRunning);
|
||||
return state.Snapshot;
|
||||
}
|
||||
|
||||
public async Task CancelAsync(Guid jobId)
|
||||
{
|
||||
if (!this.jobs.TryGetValue(jobId, out var job))
|
||||
return;
|
||||
|
||||
lock (job.SyncRoot)
|
||||
{
|
||||
if (job.IsCompletionStarted)
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!job.CancellationTokenSource.IsCancellationRequested)
|
||||
await job.CancellationTokenSource.CancelAsync();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await this.CompleteChatGenerationAsync(job, AIJobStatus.CANCELED);
|
||||
}
|
||||
|
||||
public async Task CancelChatGenerationAsync(Guid chatId)
|
||||
{
|
||||
if (!this.activeChatJobsByChatId.TryGetValue(chatId, out var jobId))
|
||||
return;
|
||||
|
||||
await this.CancelAsync(jobId);
|
||||
}
|
||||
|
||||
public async Task SetForegroundAsync(AIJobKind kind, Guid subjectId, bool isForeground)
|
||||
{
|
||||
var matchingJobs = this.jobs.Values
|
||||
.Where(job => job.Snapshot.Kind == kind && job.Snapshot.SubjectId == subjectId && job.Snapshot.IsActive)
|
||||
.ToList();
|
||||
|
||||
foreach (var job in matchingJobs)
|
||||
{
|
||||
lock (job.SyncRoot)
|
||||
{
|
||||
job.Snapshot = job.Snapshot with
|
||||
{
|
||||
IsForeground = isForeground,
|
||||
UpdatedAt = DateTimeOffset.Now,
|
||||
};
|
||||
}
|
||||
|
||||
await this.NotifyChangedAsync(job);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunChatGenerationAsync(AIJobState state)
|
||||
{
|
||||
var request = state.ChatGenerationRequest;
|
||||
var token = state.CancellationToken;
|
||||
|
||||
try
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var provider = request.ProviderSettings.CreateProvider();
|
||||
var chatThread = request.ChatThread;
|
||||
|
||||
if (!chatThread.IsLLMProviderAllowed(provider))
|
||||
{
|
||||
logger.LogError("The provider is not allowed for chat '{ChatId}' due to data security reasons. Skipping the AI process.", chatThread.ChatId);
|
||||
await this.CompleteChatGenerationAsync(state, AIJobStatus.FAILED, TB("The selected provider is not allowed for this chat."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await this.CheckSelectedModelAvailability(provider, request.ProviderSettings.Model, token))
|
||||
{
|
||||
await this.CompleteChatGenerationAsync(state, AIJobStatus.FAILED, TB("The selected model is not available."));
|
||||
return;
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var rag = new AISrcSelWithRetCtxVal();
|
||||
if (request.LastUserPrompt is not null)
|
||||
{
|
||||
chatThread = await rag.ProcessAsync(provider, request.LastUserPrompt, chatThread, token);
|
||||
request.ChatThread = chatThread;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
await this.CompleteChatGenerationAsync(state, AIJobStatus.CANCELED);
|
||||
return;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Skipping the RAG process due to an error.");
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var lastStreamingEvent = DateTimeOffset.MinValue;
|
||||
if (!TrySetWaitingForRemote(state, token))
|
||||
return;
|
||||
|
||||
await this.NotifyChangedAsync(state);
|
||||
await foreach (var contentStreamChunk in provider.StreamChatCompletion(request.ProviderSettings.Model, chatThread, settingsManager, token))
|
||||
{
|
||||
if (!TryApplyStreamChunk(state, contentStreamChunk, token))
|
||||
break;
|
||||
|
||||
var now = DateTimeOffset.Now;
|
||||
if (!settingsManager.ConfigurationData.App.IsSavingEnergy || now - lastStreamingEvent > STREAMING_EVENT_MIN_TIME)
|
||||
{
|
||||
lastStreamingEvent = now;
|
||||
await this.NotifyChangedAsync(state);
|
||||
}
|
||||
|
||||
await CheckpointChatAsync(state);
|
||||
}
|
||||
|
||||
await this.CompleteChatGenerationAsync(state, token.IsCancellationRequested ? AIJobStatus.CANCELED : AIJobStatus.COMPLETED);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
await this.CompleteChatGenerationAsync(state, AIJobStatus.CANCELED);
|
||||
}
|
||||
catch (ProviderRequestException e)
|
||||
{
|
||||
logger.LogError(e, "The provider request failed for chat generation job '{JobId}'. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", state.Snapshot.JobId, e.StatusCode, e.ReasonPhrase, e.ResponseBody);
|
||||
RemoveEmptyAIResponse(state);
|
||||
await this.CompleteChatGenerationAsync(state, AIJobStatus.FAILED, e.UserMessage);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "The chat generation job '{JobId}' failed.", state.Snapshot.JobId);
|
||||
await this.CompleteChatGenerationAsync(state, AIJobStatus.FAILED, e.Message);
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Stream, string.Format(TB("The AI job failed. The message is: '{0}'"), e.Message)));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CompleteChatGenerationAsync(AIJobState state, AIJobStatus status, string errorMessage = "")
|
||||
{
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
if (state.IsCompletionStarted)
|
||||
return;
|
||||
|
||||
state.IsCompletionStarted = true;
|
||||
}
|
||||
|
||||
var aiText = state.ChatGenerationRequest.AIText;
|
||||
aiText.InitialRemoteWait = false;
|
||||
aiText.IsStreaming = false;
|
||||
aiText.Text = aiText.Text.RemoveThinkTags().Trim();
|
||||
|
||||
RemoveEmptyAIResponse(state);
|
||||
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
state.Snapshot = state.Snapshot with
|
||||
{
|
||||
Status = status,
|
||||
ErrorMessage = errorMessage,
|
||||
UpdatedAt = DateTimeOffset.Now,
|
||||
};
|
||||
}
|
||||
|
||||
this.activeChatJobsByChatId.TryRemove(state.ChatGenerationRequest.ChatThread.ChatId, out _);
|
||||
await CheckpointChatAsync(state, force: true);
|
||||
await this.NotifyChangedAsync(state);
|
||||
await messageBus.SendMessage(null, Event.AI_JOB_FINISHED, state.Snapshot);
|
||||
state.CancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
private static void RemoveEmptyAIResponse(AIJobState state)
|
||||
{
|
||||
var aiText = state.ChatGenerationRequest.AIText;
|
||||
if (!string.IsNullOrWhiteSpace(aiText.Text))
|
||||
return;
|
||||
|
||||
var aiBlock = state.ChatGenerationRequest.ChatThread.Blocks
|
||||
.LastOrDefault(block => ReferenceEquals(block.Content, aiText));
|
||||
|
||||
if (aiBlock is not null)
|
||||
state.ChatGenerationRequest.ChatThread.Blocks.Remove(aiBlock);
|
||||
}
|
||||
|
||||
private static bool TrySetWaitingForRemote(AIJobState state, CancellationToken token)
|
||||
{
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
if (state.IsCompletionStarted || token.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
state.ChatGenerationRequest.AIText.InitialRemoteWait = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryApplyStreamChunk(AIJobState state, ContentStreamChunk contentStreamChunk, CancellationToken token)
|
||||
{
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
if (state.IsCompletionStarted || token.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
var aiText = state.ChatGenerationRequest.AIText;
|
||||
aiText.InitialRemoteWait = false;
|
||||
aiText.IsStreaming = true;
|
||||
aiText.Text += contentStreamChunk;
|
||||
aiText.Sources.MergeSources(contentStreamChunk.Sources);
|
||||
|
||||
if (state.Snapshot.Status is not AIJobStatus.RUNNING)
|
||||
{
|
||||
state.Snapshot = state.Snapshot with
|
||||
{
|
||||
Status = AIJobStatus.RUNNING,
|
||||
UpdatedAt = DateTimeOffset.Now,
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NotifyChangedAsync(AIJobState state)
|
||||
{
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
state.Snapshot = state.Snapshot with
|
||||
{
|
||||
Title = state.ChatGenerationRequest.ChatThread.Name,
|
||||
UpdatedAt = DateTimeOffset.Now,
|
||||
};
|
||||
}
|
||||
|
||||
await messageBus.SendMessage(null, Event.AI_JOB_CHANGED, state.Snapshot);
|
||||
}
|
||||
|
||||
private static async Task CheckpointChatAsync(AIJobState state, bool force = false)
|
||||
{
|
||||
var now = DateTimeOffset.Now;
|
||||
if (!force && now - state.LastCheckpoint < CHECKPOINT_MIN_TIME)
|
||||
return;
|
||||
|
||||
state.LastCheckpoint = now;
|
||||
await WorkspaceBehaviour.StoreChatAsync(state.ChatGenerationRequest.ChatThread);
|
||||
}
|
||||
|
||||
private static bool ModelsMatch(Model modelA, Model modelB)
|
||||
{
|
||||
var idA = modelA.Id.Trim();
|
||||
var idB = modelB.Id.Trim();
|
||||
return string.Equals(idA, idB, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task<bool> CheckSelectedModelAvailability(IProvider provider, Model chatModel, CancellationToken token = default)
|
||||
{
|
||||
if (chatModel.IsSystemModel)
|
||||
return true;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(chatModel.Id))
|
||||
{
|
||||
logger.LogWarning("Skipping AI request because model ID is null or white space.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!provider.HasModelLoadingCapability)
|
||||
return true;
|
||||
|
||||
IReadOnlyList<Model> loadedModels;
|
||||
try
|
||||
{
|
||||
var modelLoadResult = await provider.GetTextModels(token: token);
|
||||
if (!modelLoadResult.Success)
|
||||
{
|
||||
var userMessage = modelLoadResult.FailureReason.ToUserMessage(provider.InstanceName);
|
||||
if (!string.IsNullOrWhiteSpace(userMessage))
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, userMessage));
|
||||
|
||||
logger.LogWarning("Skipping selected model availability check for '{ProviderInstanceName}' (provider={ProviderType}) because loading the model list failed with reason {FailureReason}.", provider.InstanceName, provider.Provider, modelLoadResult.FailureReason);
|
||||
return false;
|
||||
}
|
||||
|
||||
loadedModels = modelLoadResult.Models;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning(e, "Skipping selected model availability check for '{ProviderInstanceName}' (provider={ProviderType}) because the model list could not be loaded.", provider.InstanceName, provider.Provider);
|
||||
return true;
|
||||
}
|
||||
|
||||
var availableModels = loadedModels.Where(model => !string.IsNullOrWhiteSpace(model.Id)).ToList();
|
||||
if (availableModels.Count == 0)
|
||||
{
|
||||
var emptyModelsMessage = string.Format(
|
||||
TB("We could load models from '{0}', but the provider did not return any usable text models."),
|
||||
provider.InstanceName);
|
||||
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, emptyModelsMessage));
|
||||
logger.LogWarning("Skipping AI request because there are no models available from '{ProviderInstanceName}' (provider={ProviderType}).", provider.InstanceName, provider.Provider);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (availableModels.Any(model => ModelsMatch(model, chatModel)))
|
||||
return true;
|
||||
|
||||
var message = string.Format(
|
||||
TB("The selected model '{0}' is no longer available from '{1}' (provider={2}). Please adapt your provider settings."),
|
||||
chatModel.Id,
|
||||
provider.InstanceName,
|
||||
provider.Provider);
|
||||
|
||||
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.CloudOff, message));
|
||||
logger.LogWarning("Skipping AI request because model '{ModelId}' is not available from '{ProviderInstanceName}' (provider={ProviderType}).", chatModel.Id, provider.InstanceName, provider.Provider);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
34
app/MindWork AI Studio/Tools/AIJobs/AIJobSnapshot.cs
Normal file
34
app/MindWork AI Studio/Tools/AIJobs/AIJobSnapshot.cs
Normal file
@ -0,0 +1,34 @@
|
||||
namespace AIStudio.Tools.AIJobs;
|
||||
|
||||
public sealed record AIJobSnapshot
|
||||
{
|
||||
public Guid JobId { get; init; }
|
||||
|
||||
public AIJobKind Kind { get; init; }
|
||||
|
||||
public Guid SubjectId { get; init; }
|
||||
|
||||
public Guid? ParentJobId { get; init; }
|
||||
|
||||
public Guid RootJobId { get; init; }
|
||||
|
||||
public int Priority { get; init; }
|
||||
|
||||
public bool IsForeground { get; init; }
|
||||
|
||||
public AIJobSchedulingClass SchedulingClass { get; init; }
|
||||
|
||||
public AIJobStatus Status { get; init; }
|
||||
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
public string ProviderId { get; init; } = string.Empty;
|
||||
|
||||
public string ModelId { get; init; } = string.Empty;
|
||||
|
||||
public DateTimeOffset UpdatedAt { get; init; }
|
||||
|
||||
public string ErrorMessage { get; init; } = string.Empty;
|
||||
|
||||
public bool IsActive => this.Status is AIJobStatus.QUEUED or AIJobStatus.WAITING_FOR_REMOTE or AIJobStatus.RUNNING;
|
||||
}
|
||||
12
app/MindWork AI Studio/Tools/AIJobs/AIJobStatus.cs
Normal file
12
app/MindWork AI Studio/Tools/AIJobs/AIJobStatus.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace AIStudio.Tools.AIJobs;
|
||||
|
||||
public enum AIJobStatus
|
||||
{
|
||||
NONE,
|
||||
QUEUED,
|
||||
WAITING_FOR_REMOTE,
|
||||
RUNNING,
|
||||
COMPLETED,
|
||||
CANCELED,
|
||||
FAILED,
|
||||
}
|
||||
20
app/MindWork AI Studio/Tools/AIJobs/ChatGenerationRequest.cs
Normal file
20
app/MindWork AI Studio/Tools/AIJobs/ChatGenerationRequest.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using AIStudio.Chat;
|
||||
|
||||
namespace AIStudio.Tools.AIJobs;
|
||||
|
||||
public sealed record ChatGenerationRequest
|
||||
{
|
||||
public required ChatThread ChatThread { get; set; }
|
||||
|
||||
public required ContentText AIText { get; init; }
|
||||
|
||||
public IContent? LastUserPrompt { get; init; }
|
||||
|
||||
public required AIStudio.Settings.Provider ProviderSettings { get; init; }
|
||||
|
||||
public Guid? ParentJobId { get; init; }
|
||||
|
||||
public int Priority { get; init; }
|
||||
|
||||
public bool IsForeground { get; init; } = true;
|
||||
}
|
||||
@ -4,7 +4,11 @@ public abstract class EmbeddingStore(string name, string path)
|
||||
{
|
||||
public string Name => name;
|
||||
|
||||
public virtual bool IsAvailable => true;
|
||||
public virtual string CacheKey => name;
|
||||
|
||||
public virtual DatabaseClientStatus Status => DatabaseClientStatus.AVAILABLE;
|
||||
|
||||
public bool IsAvailable => this.Status is DatabaseClientStatus.AVAILABLE;
|
||||
|
||||
private string Path => path;
|
||||
|
||||
@ -50,15 +54,6 @@ public abstract class EmbeddingStore(string name, string path)
|
||||
{
|
||||
this.logger = logService;
|
||||
}
|
||||
|
||||
|
||||
public abstract Task EnsureEmbeddingStoreExists(string collectionName, int vectorSize, CancellationToken token);
|
||||
|
||||
public abstract Task InsertEmbedding(string collectionName, IReadOnlyList<EmbeddingStoragePoint> points, CancellationToken token);
|
||||
|
||||
public abstract Task DeleteEmbeddingByFile(string collectionName, string filePath, CancellationToken token);
|
||||
|
||||
public abstract Task DeleteEmbeddingStore(string collectionName, CancellationToken token);
|
||||
|
||||
public abstract void Dispose();
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
using AIStudio.Tools.Rust;
|
||||
|
||||
namespace AIStudio.Tools.Databases;
|
||||
|
||||
public sealed partial class DatabaseClientProvider
|
||||
{
|
||||
private async Task<DatabaseClient> CreateQdrantClientAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var qdrantInfo = await this.rustService.GetQdrantInfo(cancellationToken);
|
||||
if (qdrantInfo.Status is QdrantStatus.STARTING)
|
||||
{
|
||||
return this.CreateNoDatabaseClient(
|
||||
"Qdrant",
|
||||
"Qdrant is starting. Details will appear shortly.",
|
||||
DatabaseClientStatus.STARTING);
|
||||
}
|
||||
|
||||
if (!qdrantInfo.IsAvailable || qdrantInfo.Status is QdrantStatus.UNAVAILABLE)
|
||||
{
|
||||
var reason = qdrantInfo.UnavailableReason ?? "unknown";
|
||||
this.logger.LogWarning("Qdrant is not available. Starting without vector database. Reason: '{Reason}'.", reason);
|
||||
return this.CreateNoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason, DatabaseClientStatus.UNAVAILABLE);
|
||||
}
|
||||
|
||||
if (!HasValidQdrantConnectionInfo(qdrantInfo, out var invalidReason))
|
||||
return this.CreateNoDatabaseClient("Qdrant", invalidReason, DatabaseClientStatus.UNAVAILABLE);
|
||||
|
||||
var client = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken);
|
||||
client.SetLogger(this.databaseClientLogger);
|
||||
|
||||
try
|
||||
{
|
||||
await client.CheckAvailabilityAsync();
|
||||
return client;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
client.Dispose();
|
||||
this.logger.LogWarning(e, "Qdrant reported as available by Rust, but the health check failed.");
|
||||
return this.CreateNoDatabaseClient("Qdrant", e.Message, DatabaseClientStatus.STARTING);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasValidQdrantConnectionInfo(QdrantInfo qdrantInfo, out string invalidReason)
|
||||
{
|
||||
if (qdrantInfo.Path == string.Empty)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant path from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.PortHttp == 0)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant HTTP port from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.PortGrpc == 0)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant gRPC port from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.Fingerprint == string.Empty)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant fingerprint from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.ApiToken == string.Empty)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant API token from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
invalidReason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
180
app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs
Normal file
180
app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs
Normal file
@ -0,0 +1,180 @@
|
||||
using AIStudio.Tools.Databases.Qdrant;
|
||||
using AIStudio.Tools.Rust;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tools.Databases;
|
||||
|
||||
public sealed class EmbeddingStoreProvider(RustService rustService, ILoggerFactory loggerFactory) : IDisposable
|
||||
{
|
||||
private readonly Dictionary<DatabaseRole, EmbeddingStore> clients = new();
|
||||
private readonly Dictionary<DatabaseRole, SemaphoreSlim> locks = new();
|
||||
private readonly Lock locksLock = new();
|
||||
private readonly ILogger<EmbeddingStoreProvider> logger = loggerFactory.CreateLogger<EmbeddingStoreProvider>();
|
||||
private readonly ILogger<DatabaseClient> databaseClientLogger = loggerFactory.CreateLogger<DatabaseClient>();
|
||||
|
||||
public async Task<DatabaseClient> GetClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var databaseLock = this.GetLock(databaseRole);
|
||||
await databaseLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (this.clients.TryGetValue(databaseRole, out var cachedClient) && cachedClient.IsAvailable)
|
||||
return cachedClient;
|
||||
|
||||
var client = await this.CreateClientAsync(databaseRole, cancellationToken);
|
||||
return this.CacheIfAvailable(databaseRole, client);
|
||||
}
|
||||
finally
|
||||
{
|
||||
databaseLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DatabaseClient> RefreshClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var databaseLock = this.GetLock(databaseRole);
|
||||
await databaseLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
var client = await this.CreateClientAsync(databaseRole, cancellationToken);
|
||||
return this.CacheIfAvailable(databaseRole, client);
|
||||
}
|
||||
finally
|
||||
{
|
||||
databaseLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseClient CacheIfAvailable(DatabaseRole databaseRole, DatabaseClient client)
|
||||
{
|
||||
if (!client.IsAvailable)
|
||||
return client;
|
||||
|
||||
if (this.clients.TryGetValue(databaseRole, out var cachedClient))
|
||||
{
|
||||
if (IsSameClient(cachedClient, client))
|
||||
{
|
||||
client.Dispose();
|
||||
return cachedClient;
|
||||
}
|
||||
|
||||
cachedClient.Dispose();
|
||||
}
|
||||
|
||||
this.clients[databaseRole] = client;
|
||||
return client;
|
||||
}
|
||||
|
||||
private SemaphoreSlim GetLock(DatabaseRole databaseRole)
|
||||
{
|
||||
lock (this.locksLock)
|
||||
{
|
||||
if (this.locks.TryGetValue(databaseRole, out var databaseLock))
|
||||
return databaseLock;
|
||||
|
||||
databaseLock = new SemaphoreSlim(1, 1);
|
||||
this.locks[databaseRole] = databaseLock;
|
||||
return databaseLock;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<DatabaseClient> CreateClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken) => databaseRole switch
|
||||
{
|
||||
DatabaseRole.VECTOR_STORE => await this.CreateQdrantClientAsync(cancellationToken),
|
||||
_ => new NoDatabaseClient(databaseRole.ToString(), "The requested database role is not supported.")
|
||||
};
|
||||
|
||||
private async Task<DatabaseClient> CreateQdrantClientAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var qdrantInfo = await rustService.GetQdrantInfo(cancellationToken);
|
||||
if (qdrantInfo.Status is QdrantStatus.STARTING)
|
||||
{
|
||||
return this.CreateNoDatabaseClient(
|
||||
"Qdrant",
|
||||
"Qdrant is starting. Details will appear shortly.",
|
||||
DatabaseClientStatus.STARTING);
|
||||
}
|
||||
|
||||
if (!qdrantInfo.IsAvailable || qdrantInfo.Status is QdrantStatus.UNAVAILABLE)
|
||||
{
|
||||
var reason = qdrantInfo.UnavailableReason ?? "unknown";
|
||||
this.logger.LogWarning("Qdrant is not available. Starting without vector database. Reason: '{Reason}'.", reason);
|
||||
return this.CreateNoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason, DatabaseClientStatus.UNAVAILABLE);
|
||||
}
|
||||
|
||||
if (!HasValidQdrantConnectionInfo(qdrantInfo, out var invalidReason))
|
||||
return this.CreateNoDatabaseClient("Qdrant", invalidReason, DatabaseClientStatus.UNAVAILABLE);
|
||||
|
||||
var client = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken);
|
||||
client.SetLogger(this.databaseClientLogger);
|
||||
|
||||
try
|
||||
{
|
||||
await client.CheckAvailabilityAsync();
|
||||
return client;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
client.Dispose();
|
||||
this.logger.LogWarning(e, "Qdrant reported as available by Rust, but the health check failed.");
|
||||
return this.CreateNoDatabaseClient("Qdrant", e.Message, DatabaseClientStatus.STARTING);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasValidQdrantConnectionInfo(QdrantInfo qdrantInfo, out string invalidReason)
|
||||
{
|
||||
if (qdrantInfo.Path == string.Empty)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant path from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.PortHttp == 0)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant HTTP port from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.PortGrpc == 0)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant gRPC port from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.Fingerprint == string.Empty)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant fingerprint from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (qdrantInfo.ApiToken == string.Empty)
|
||||
{
|
||||
invalidReason = "Failed to get the Qdrant API token from Rust.";
|
||||
return false;
|
||||
}
|
||||
|
||||
invalidReason = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private NoDatabaseClient CreateNoDatabaseClient(string name, string? unavailableReason, DatabaseClientStatus status)
|
||||
{
|
||||
var client = new NoDatabaseClient(name, unavailableReason, status);
|
||||
client.SetLogger(this.databaseClientLogger);
|
||||
return client;
|
||||
}
|
||||
|
||||
private static bool IsSameClient(DatabaseClient left, DatabaseClient right) =>
|
||||
left.IsAvailable
|
||||
&& right.IsAvailable
|
||||
&& left.CacheKey == right.CacheKey;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var client in this.clients.Values)
|
||||
client.Dispose();
|
||||
|
||||
foreach (var databaseLock in this.locks.Values)
|
||||
databaseLock.Dispose();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace AIStudio.Tools.Databases;
|
||||
|
||||
public enum DatabaseClientStatus
|
||||
{
|
||||
STARTING,
|
||||
AVAILABLE,
|
||||
UNAVAILABLE,
|
||||
}
|
||||
6
app/MindWork AI Studio/Tools/Databases/DatabaseRole.cs
Normal file
6
app/MindWork AI Studio/Tools/Databases/DatabaseRole.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace AIStudio.Tools.Databases;
|
||||
|
||||
public enum DatabaseRole
|
||||
{
|
||||
VECTOR_STORE,
|
||||
}
|
||||
23
app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs
Normal file
23
app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Tools.Databases;
|
||||
|
||||
public sealed class NoEmbeddingStore(string name, string? unavailableReason, DatabaseClientStatus status = DatabaseClientStatus.UNAVAILABLE) : EmbeddingStore(name, string.Empty)
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoEmbeddingStore).Namespace, nameof(NoEmbeddingStore));
|
||||
|
||||
public override DatabaseClientStatus Status => status;
|
||||
|
||||
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
|
||||
{
|
||||
yield return (TB("Status"), TB("Unavailable"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(unavailableReason))
|
||||
yield return (TB("Reason"), unavailableReason);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
public override void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
namespace AIStudio.Tools.Databases;
|
||||
|
||||
public sealed class NoEmbeddingStore(string name, string? unavailableReason) : EmbeddingStore(name, string.Empty)
|
||||
{
|
||||
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoEmbeddingStore).Namespace, nameof(NoEmbeddingStore));
|
||||
|
||||
public override bool IsAvailable => false;
|
||||
|
||||
public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo()
|
||||
{
|
||||
yield return (TB("Status"), TB("Unavailable"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(unavailableReason))
|
||||
yield return (TB("Reason"), unavailableReason);
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task EnsureEmbeddingStoreExists(string collectionName, int vectorSize, CancellationToken token) => throw this.BuildUnavailableException();
|
||||
|
||||
public override Task InsertEmbedding(string collectionName, IReadOnlyList<EmbeddingStoragePoint> points, CancellationToken token) => throw this.BuildUnavailableException();
|
||||
|
||||
public override Task DeleteEmbeddingByFile(string collectionName, string filePath, CancellationToken token) => Task.CompletedTask;
|
||||
|
||||
public override Task DeleteEmbeddingStore(string collectionName, CancellationToken token) => Task.CompletedTask;
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private InvalidOperationException BuildUnavailableException()
|
||||
{
|
||||
return new InvalidOperationException(string.IsNullOrWhiteSpace(unavailableReason)
|
||||
? "The vector database is not available."
|
||||
: unavailableReason);
|
||||
}
|
||||
}
|
||||
@ -28,6 +28,8 @@ public class QdrantClientImplementation : EmbeddingStore
|
||||
this.ApiToken = apiToken ?? string.Empty;
|
||||
this.GrpcClient = this.CreateQdrantClient();
|
||||
}
|
||||
|
||||
public override string CacheKey => $"{this.Name}:{this.HttpPort}:{this.GrpcPort}:{this.Fingerprint}";
|
||||
|
||||
private const string IP_ADDRESS = "localhost";
|
||||
|
||||
@ -49,6 +51,11 @@ public class QdrantClientImplementation : EmbeddingStore
|
||||
return $"v{operation.Version}";
|
||||
}
|
||||
|
||||
public async Task CheckAvailabilityAsync()
|
||||
{
|
||||
await this.GrpcClient.HealthAsync();
|
||||
}
|
||||
|
||||
private async Task<string> GetCollectionsAmount()
|
||||
{
|
||||
var operation = await this.GrpcClient.ListCollectionsAsync();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user