Merge branch 'main' into rag-extract-data

This commit is contained in:
Thorsten Sommer 2025-04-01 19:10:08 +02:00 committed by GitHub
commit 94ce409e3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
907 changed files with 13971 additions and 2061 deletions

8
.github/CODEOWNERS vendored
View File

@ -2,13 +2,13 @@
* @MindWorkAI/maintainer * @MindWorkAI/maintainer
# The release team is responsible for anything inside the .github directory, such as workflows, actions, and issue templates: # The release team is responsible for anything inside the .github directory, such as workflows, actions, and issue templates:
/.github/ @MindWorkAI/release /.github/ @MindWorkAI/release @SommerEngineering
# The release team is responsible for the update directory:
/.updates/ @MindWorkAI/release
# Our Rust experts are responsible for the Rust codebase: # Our Rust experts are responsible for the Rust codebase:
/runtime/ @MindWorkAI/rust-experts /runtime/ @MindWorkAI/rust-experts
# Our .NET experts are responsible for the .NET codebase: # Our .NET experts are responsible for the .NET codebase:
/app/ @MindWorkAI/net-experts /app/ @MindWorkAI/net-experts
# The source code rules must be reviewed by the release team:
/app/SourceCodeRules/ @MindWorkAI/release @SommerEngineering

1
.github/FUNDING.yml vendored
View File

@ -1,2 +1 @@
github: [MindWorkAI] github: [MindWorkAI]
open_collective: mindwork-ai

View File

@ -1,6 +1,8 @@
name: Build and Release name: Build and Release
on: on:
push: push:
branches:
- main
tags: tags:
- "v*.*.*" - "v*.*.*"
@ -46,6 +48,7 @@ jobs:
echo "version=${version}" >> "$GITHUB_OUTPUT" echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Check tag vs. metadata version - name: Check tag vs. metadata version
if: startsWith(github.ref, 'refs/tags/v')
run: | run: |
# Ensure, that the tag matches the version in the metadata file: # Ensure, that the tag matches the version in the metadata file:
if [ "${GITHUB_REF}" != "refs/tags/${FORMATTED_VERSION}" ]; then if [ "${GITHUB_REF}" != "refs/tags/${FORMATTED_VERSION}" ]; then
@ -292,6 +295,35 @@ jobs:
Write-Output "Tauri is already installed" Write-Output "Tauri 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*
- name: Delete previous artifact, which may exist due to caching (Windows - MSI)
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi')
run: |
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/msi/MindWork AI Studio_*.msi" -ErrorAction SilentlyContinue
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/msi/MindWork AI Studio*msi.zip*" -ErrorAction SilentlyContinue
- name: Delete previous artifact, which may exist due to caching (Windows - NSIS)
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis')
run: |
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*
- name: Build Tauri project (Unix) - name: Build Tauri project (Unix)
if: matrix.platform != 'windows-latest' if: matrix.platform != 'windows-latest'
env: env:
@ -315,7 +347,7 @@ jobs:
cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }} cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }}
- name: Upload artifact (macOS) - name: Upload artifact (macOS)
if: startsWith(matrix.platform, 'macos') if: startsWith(matrix.platform, 'macos') && startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }}) name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }})
@ -326,7 +358,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- name: Upload artifact (Windows - MSI) - name: Upload artifact (Windows - MSI)
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi') if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi') && startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (Windows - MSI ${{ matrix.dotnet_runtime }}) name: MindWork AI Studio (Windows - MSI ${{ matrix.dotnet_runtime }})
@ -337,7 +369,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- name: Upload artifact (Windows - NSIS) - name: Upload artifact (Windows - NSIS)
if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis') if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'nsis') && startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (Windows - NSIS ${{ matrix.dotnet_runtime }}) name: MindWork AI Studio (Windows - NSIS ${{ matrix.dotnet_runtime }})
@ -348,7 +380,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- name: Upload artifact (Linux - Debian Package) - name: Upload artifact (Linux - Debian Package)
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb') if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb') && startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (Linux - deb ${{ matrix.dotnet_runtime }}) name: MindWork AI Studio (Linux - deb ${{ matrix.dotnet_runtime }})
@ -358,7 +390,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }} retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- name: Upload artifact (Linux - AppImage) - name: Upload artifact (Linux - AppImage)
if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage') if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage') && startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }}) name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }})
@ -507,6 +539,14 @@ jobs:
run: | run: |
mv ${{ steps.build-linux-arm-runner.outputs.image }} ${{ runner.temp }}/linux_arm_qemu_cache.img mv ${{ steps.build-linux-arm-runner.outputs.image }} ${{ runner.temp }}/linux_arm_qemu_cache.img
#
# This step does not work, because we start a VM with qemu to run the build.
#
#- name: Delete previous artifact, which may exist due to caching (Linux - Debian Package)
# if: ${{ env.SKIP != 'true' }}
# run: |
# rm -f result/target/aarch64-unknown-linux-gnu/release/bundle/deb/mind-work-ai-studio_*.deb
- name: Build Tauri project - name: Build Tauri project
if: ${{ env.SKIP != 'true' }} if: ${{ env.SKIP != 'true' }}
uses: pguyot/arm-runner-action@v2 uses: pguyot/arm-runner-action@v2
@ -531,6 +571,9 @@ jobs:
shell: /bin/bash shell: /bin/bash
commands: | commands: |
# Delete all previous artifacts, which may exist due to caching:
rm -f runtime/target/aarch64-unknown-linux-gnu/release/bundle/deb/mind-work-ai-studio_*.deb
export HOME=/root export HOME=/root
export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
source "$HOME/.cargo/env" source "$HOME/.cargo/env"
@ -553,7 +596,7 @@ jobs:
mv ${{ steps.build-linux-arm.outputs.image }} $RUNNER_TEMP/linux_arm_qemu_cache.img mv ${{ steps.build-linux-arm.outputs.image }} $RUNNER_TEMP/linux_arm_qemu_cache.img
- name: Upload artifact (Linux - Debian Package) - name: Upload artifact (Linux - Debian Package)
if: ${{ env.SKIP != 'true' }} if: ${{ env.SKIP != 'true' && startsWith(github.ref, 'refs/tags/v') }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: MindWork AI Studio (Linux - deb linux-arm64) name: MindWork AI Studio (Linux - deb linux-arm64)
@ -566,6 +609,7 @@ jobs:
name: Prepare & create release name: Prepare & create release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_main, read_metadata, build_linux_arm64] needs: [build_main, read_metadata, build_linux_arm64]
if: startsWith(github.ref, 'refs/tags/v')
steps: steps:
- name: Create artifact directory - name: Create artifact directory
run: mkdir -p $GITHUB_WORKSPACE/artifacts run: mkdir -p $GITHUB_WORKSPACE/artifacts
@ -723,6 +767,7 @@ jobs:
name: Publish release name: Publish release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [read_metadata, create_release] needs: [read_metadata, create_release]
if: startsWith(github.ref, 'refs/tags/v')
permissions: permissions:
contents: write contents: write

View File

@ -1,4 +1,5 @@
# MindWork AI Studio # MindWork AI Studio
Are you new here? [Read here](#what-is-ai-studio) what AI Studio is.
## News ## News
Things we are currently working on: Things we are currently working on:
@ -11,16 +12,16 @@ Things we are currently working on:
- [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~ - [x] ~~App: Add an option to show preview features (PR [#222](https://github.com/MindWorkAI/AI-Studio/pull/222))~~
- [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~ - [x] ~~App: Configure embedding providers (PR [#224](https://github.com/MindWorkAI/AI-Studio/pull/224))~~
- [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: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
- [ ] App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) - [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))~~
- [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files - [ ] Runtime: Extract data from txt / md / pdf / docx / xlsx files
- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs) - [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs)
- [ ] App: Implement external embedding providers - [ ] App: Implement external embedding providers
- [ ] App: Implement the process to vectorize one local file using embeddings - [ ] App: Implement the process to vectorize one local file using embeddings
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb) - [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
- [ ] App: Implement the continuous process of vectorizing data - [ ] App: Implement the continuous process of vectorizing data
- [ ] App: Define a common retrieval context interface for the integration of RAG processes in chats - [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))~~
- [ ] App: Define a common augmentation interface for the integration of RAG processes in chats - [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))~~
- [ ] App: Integrate data sources in chats - [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~
- Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[#167](https://github.com/MindWorkAI/AI-Studio/pull/167), [#226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~. - Since September 2024: Experiments have been started on how we can work on long texts with AI Studio. Let's say you want to write a fantasy novel or create a complex project proposal and use LLM for support. The initial experiments were promising, but not yet satisfactory. We are testing further approaches until a satisfactory solution is found. The current state of our experiment is available as an experimental preview feature through your app configuration. Related PR: ~~[#167](https://github.com/MindWorkAI/AI-Studio/pull/167), [#226](https://github.com/MindWorkAI/AI-Studio/pull/226)~~.
@ -49,14 +50,17 @@ Features we have recently released:
![MindWork AI Studio - Home](documentation/AI%20Studio%20Home.png) ![MindWork AI Studio - Home](documentation/AI%20Studio%20Home.png)
![MindWork AI Studio - Assistants](documentation/AI%20Studio%20Assistants.png) ![MindWork AI Studio - Assistants](documentation/AI%20Studio%20Assistants.png)
MindWork AI Studio is a desktop application available for macOS, Windows, and Linux. It provides a unified chat interface for Large Language Models (LLMs). You bring your own API key for the respective LLM provider to use the models. The API keys are securely stored by the operating system. MindWork AI Studio is a free desktop app for macOS, Windows, and Linux. It provides a unified user interface for interaction with Large Language Models (LLM). AI Studio also offers so-called assistants, where prompting is not necessary. You can think of AI Studio like an email program: you bring your own API key for the LLM of your choice and can then use these AI systems with AI Studio. Whether you want to use Google Gemini, OpenAI o1, or even your own local AI models.
**Ready to get started 🤩?** [Download the appropriate setup for your operating system here](documentation/Setup.md).
**Key advantages:** **Key advantages:**
- **Free of charge**: The app is free to use, both for personal and commercial purposes. - **Free of charge**: The app is free to use, both for personal and commercial purposes.
- **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/). - **Independence**: You are not tied to any single provider. Instead, you can choose the provider that best suits their needs. Right now, we support OpenAI (GPT4o, o1, etc.), Mistral, Anthropic (Claude), Google Gemini, xAI (Grok), DeepSeek, and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), [Groq](https://groq.com/), or [Fireworks](https://fireworks.ai/). For scientists and employees of research institutions, we also support [Helmholtz](https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570) and [GWDG](https://gwdg.de/services/application-services/ai-services/) AI services. These are available through federated logins like eduGAIN to all 18 Helmholtz Centers, the Max Planck Society, most German, and many international universities.
- **Assistants**: You just want to quickly translate a text? AI Studio has so-called assistants for such and other tasks. No prompting is necessary when working with these assistants.
- **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API. - **Unrestricted usage**: Unlike services like ChatGPT, which impose limits after intensive use, MindWork AI Studio offers unlimited usage through the providers API.
- **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit. - **Cost-effective**: You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
- **Privacy**: The data entered into the app is not used for training by the providers since we are using the provider's API. - **Privacy**: You can control which providers receive your data using the provider confidence settings. For example, you can set different protection levels for writing emails compared to general chats, etc. Additionally, most providers guarantee that they won't use your data to train new AI systems.
- **Flexibility**: Choose the provider and model best suited for your current task. - **Flexibility**: Choose the provider and model best suited for your current task.
- **No bloatware**: The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life. - **No bloatware**: The app requires minimal storage for installation and operates with low memory usage. Additionally, it has a minimal impact on system resources, which is beneficial for battery life.
@ -78,12 +82,15 @@ Your support, whether big or small, keeps the wheels turning and is deeply appre
## Planned Features ## Planned Features
Here's an exciting look at some of the features we're planning to add to AI Studio in future releases: Here's an exciting look at some of the features we're planning to add to AI Studio in future releases:
- **Integrating your data**: You should be able to integrate your data into AI Studio. For example, your PDF or Office files, or your Markdown notes. - **Integrating your data**: You'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes.
- **Integration of enterprise data:** Soon, it will also be possible to integrate data from the corporate network using an interface that we have specified ([External Retrieval Interface](https://github.com/MindWorkAI/ERI), ERI for short). - **Integration of enterprise data:** It will soon be possible to integrate data from the corporate network using a specified interface ([External Retrieval Interface](https://github.com/MindWorkAI/ERI), ERI for short). This will likely require development work by the organization in question.
- **Writing mode:** We want to integrate a writing mode that should support you in creating extensive works. We are thinking of comprehensive project proposals, tenders, or your next fantasy novel. - **Useful assistants:** We'll develop more assistants for everyday tasks.
- **Browser usage:** We're trying to offer the features from AI Studio to you in the browser via a plugin, so we could use spell-checking or rewriting text directly in the browser. - **Writing mode:** We're integrating a writing mode to help you create extensive works, like comprehensive project proposals, tenders, or your next fantasy novel.
- **Voice control:** You should be able to interact with the AI systems using your voice as well. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation. - **Specific requirements:** Want an assistant that suits your specific needs? We aim to offer a plugin architecture so organizations and enthusiasts can implement such ideas.
- **Email monitoring:** You should have the option to connect your email inboxes with AI Studio. The AI reads your emails and sends you a notification when something important happens. At the same time, you can access knowledge from your emails in your chats. - **Voice control:** You'll interact with the AI systems using your voice. To achieve this, we want to integrate voice input (speech-to-text) and output (text-to-speech). However, later on, it should also have a natural conversation flow, i.e., seamless conversation.
- **Content creation:** There will be an interface for AI Studio to create content in other apps. You could, for example, create blog posts directly on the target platform or add entries to an internal knowledge management tool. This requires development work by the tool developers.
- **Email monitoring:** You can connect your email inboxes with AI Studio. The AI will read your emails and notify you of important events. You'll also be able to access knowledge from your emails in your chats.
- **Browser usage:** We're working on offering AI Studio features in your browser via a plugin, allowing, e.g., for spell-checking or text rewriting directly in the browser.
Stay tuned for more updates and enhancements to make MindWork AI Studio even more powerful and versatile 🤩. Stay tuned for more updates and enhancements to make MindWork AI Studio even more powerful and versatile 🤩.

View File

@ -2,6 +2,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MindWork AI Studio", "MindWork AI Studio\MindWork AI Studio.csproj", "{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceCodeRules", "SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj", "{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -12,5 +14,11 @@ Global
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU {059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU {059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU {059FDFCC-7D0B-474E-9F20-B9C437DF1CDD}.Release|Any CPU.Build.0 = Release|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -2,9 +2,15 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String> <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/=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/=ERI/@EntryIndexedValue">ERI</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/=LLM/@EntryIndexedValue">LLM</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LM/@EntryIndexedValue">LM</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=MSG/@EntryIndexedValue">MSG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=RAG/@EntryIndexedValue">RAG</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=UI/@EntryIndexedValue">UI</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=agentic/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=groq/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gwdg/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mwais/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=ollama/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -1,13 +1,31 @@
using System.Text.Json;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Services;
// ReSharper disable MemberCanBePrivate.Global // ReSharper disable MemberCanBePrivate.Global
namespace AIStudio.Agents; namespace AIStudio.Agents;
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, ThreadSafeRandom rng) : IAgent public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : IAgent
{ {
protected static readonly ContentBlock EMPTY_BLOCK = new()
{
Content = null,
ContentType = ContentType.NONE,
Role = ChatRole.AGENT,
Time = DateTimeOffset.UtcNow,
};
protected static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
protected DataSourceService DataSourceService { get; init; } = dataSourceService;
protected SettingsManager SettingsManager { get; init; } = settingsManager; protected SettingsManager SettingsManager { get; init; } = settingsManager;
protected ThreadSafeRandom RNG { get; init; } = rng; protected ThreadSafeRandom RNG { get; init; } = rng;
@ -60,24 +78,30 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
Blocks = [], Blocks = [],
}; };
protected DateTimeOffset AddUserRequest(ChatThread thread, string request) protected UserRequest AddUserRequest(ChatThread thread, string request)
{ {
var time = DateTimeOffset.Now; var time = DateTimeOffset.Now;
var lastUserPrompt = new ContentText
{
Text = request,
};
thread.Blocks.Add(new ContentBlock thread.Blocks.Add(new ContentBlock
{ {
Time = time, Time = time,
ContentType = ContentType.TEXT, ContentType = ContentType.TEXT,
Role = ChatRole.USER, Role = ChatRole.USER,
Content = new ContentText Content = lastUserPrompt,
{
Text = request,
},
}); });
return time; return new()
{
Time = time,
UserPrompt = lastUserPrompt,
};
} }
protected async Task AddAIResponseAsync(ChatThread thread, DateTimeOffset time) protected async Task AddAIResponseAsync(ChatThread thread, IContent lastUserPrompt, DateTimeOffset time)
{ {
if(this.ProviderSettings is null) if(this.ProviderSettings is null)
return; return;
@ -103,6 +127,6 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
// Use the selected provider to get the AI response. // Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire // By awaiting this line, we wait for the entire
// content to be streamed. // content to be streamed.
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), this.SettingsManager, providerSettings.Model, thread); await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), providerSettings.Model, lastUserPrompt, thread);
} }
} }

View File

@ -0,0 +1,378 @@
using System.Text;
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient;
using AIStudio.Tools.Services;
namespace AIStudio.Agents;
public sealed class AgentDataSourceSelection (ILogger<AgentDataSourceSelection> logger, ILogger<AgentBase> baseLogger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : AgentBase(baseLogger, settingsManager, dataSourceService, rng)
{
private readonly List<ContentBlock> answers = new();
#region Overrides of AgentBase
/// <inheritdoc />
protected override Type Type => Type.SYSTEM;
/// <inheritdoc />
public override string Id => "Data Source Selection";
/// <inheritdoc />
protected override string JobDescription =>
"""
You receive a system and a user prompt, as well as a list of possible data sources as input.
Your task is to select the appropriate data sources for the given task. You may choose none,
one, or multiple sources, depending on what best fits the system and user prompt. You need
to estimate and assess which source, based on its description, might be helpful in
processing the prompts.
Your response is a JSON list in the following format:
```
[
{"id": "The data source ID", "reason": "Why did you choose this source?", "confidence": 0.87},
{"id": "The data source ID", "reason": "Why did you choose this source?", "confidence": 0.54}
]
```
You express your confidence as a floating-point number between 0.0 (maximum uncertainty) and
1.0 (you are absolutely certain that this source is needed).
The JSON schema is:
```
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"items": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"reason": {
"type": "string"
},
"confidence": {
"type": "number"
}
},
"required": [
"id",
"reason",
"confidence"
]
}
]
}
```
When no data source is needed, you return an empty JSON list `[]`. You do not ask any
follow-up questions. You do not address the user. Your response consists solely of
the JSON list.
""";
/// <inheritdoc />
protected override string SystemPrompt(string availableDataSources) => $"""
{this.JobDescription}
{availableDataSources}
""";
/// <inheritdoc />
public override Settings.Provider? ProviderSettings { get; set; }
/// <summary>
/// The data source selection agent does not work with context. Use
/// the process input method instead.
/// </summary>
/// <returns>The chat thread without any changes.</returns>
public override Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData) => Task.FromResult(chatThread);
/// <inheritdoc />
public override async Task<ContentBlock> ProcessInput(ContentBlock input, IDictionary<string, string> additionalData)
{
if (input.Content is not ContentText text)
return EMPTY_BLOCK;
if(text.InitialRemoteWait || text.IsStreaming)
return EMPTY_BLOCK;
if(string.IsNullOrWhiteSpace(text.Text))
return EMPTY_BLOCK;
if(!additionalData.TryGetValue("availableDataSources", out var availableDataSources) || string.IsNullOrWhiteSpace(availableDataSources))
return EMPTY_BLOCK;
var thread = this.CreateChatThread(this.SystemPrompt(availableDataSources));
var userRequest = this.AddUserRequest(thread, text.Text);
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
var answer = thread.Blocks[^1];
this.answers.Add(answer);
return answer;
}
// <inheritdoc />
public override Task<bool> MadeDecision(ContentBlock input) => Task.FromResult(true);
// <inheritdoc />
public override IReadOnlyCollection<ContentBlock> GetContext() => [];
// <inheritdoc />
public override IReadOnlyCollection<ContentBlock> GetAnswers() => this.answers;
#endregion
public async Task<List<SelectedDataSource>> PerformSelectionAsync(IProvider provider, IContent lastPrompt, ChatThread chatThread, AllowedSelectedDataSources dataSources, CancellationToken token = default)
{
logger.LogInformation("The AI should select the appropriate data sources.");
//
// 1. Which LLM provider should the agent use?
//
// We start with the provider currently selected by the user:
var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_DATA_SOURCE_SELECTION, provider.Id, true);
// Assign the provider settings to the agent:
logger.LogInformation($"The agent for the data source selection uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()}).");
this.ProviderSettings = agentProvider;
//
// 2. Prepare the current system and user prompts as input for the agent:
//
var lastPromptContent = lastPrompt switch
{
ContentText text => text.Text,
// Image prompts may be empty, e.g., when the image is too large:
ContentImage image => await image.AsBase64(token),
// Other content types are not supported yet:
_ => string.Empty,
};
if (string.IsNullOrWhiteSpace(lastPromptContent))
{
logger.LogWarning("The last prompt is empty. The AI cannot select data sources.");
return [];
}
//
// 3. Prepare the allowed data sources as input for the agent:
//
var additionalData = new Dictionary<string, string>();
logger.LogInformation("Preparing the list of allowed data sources for the agent to choose from.");
// Notice: We do not dispose the Rust service here. The Rust service is a singleton
// and will be disposed when the application shuts down:
var rustService = Program.SERVICE_PROVIDER.GetService<RustService>()!;
var sb = new StringBuilder();
sb.AppendLine("The following data sources are available for selection:");
foreach (var ds in dataSources.AllowedDataSources)
{
switch (ds)
{
case DataSourceLocalDirectory localDirectory:
sb.AppendLine($"- Id={ds.Id}, name='{localDirectory.Name}', type=local directory, path='{localDirectory.Path}'");
break;
case DataSourceLocalFile localFile:
sb.AppendLine($"- Id={ds.Id}, name='{localFile.Name}', type=local file, path='{localFile.FilePath}'");
break;
case IERIDataSource eriDataSource:
var eriServerDescription = string.Empty;
try
{
//
// Call the ERI server to get the server description:
//
using var eriClient = ERIClientFactory.Get(eriDataSource.Version, eriDataSource)!;
var authResponse = await eriClient.AuthenticateAsync(rustService, cancellationToken: token);
if (authResponse.Successful)
{
var serverDescriptionResponse = await eriClient.GetDataSourceInfoAsync(token);
if (serverDescriptionResponse.Successful)
{
eriServerDescription = serverDescriptionResponse.Data.Description;
// Remove all line breaks from the description:
eriServerDescription = eriServerDescription.Replace("\n", " ").Replace("\r", " ");
}
else
logger.LogWarning($"Was not able to retrieve the server description from the ERI data source '{eriDataSource.Name}'. Message: {serverDescriptionResponse.Message}");
}
else
logger.LogWarning($"Was not able to authenticate with the ERI data source '{eriDataSource.Name}'. Message: {authResponse.Message}");
}
catch (Exception e)
{
logger.LogWarning($"The ERI data source '{eriDataSource.Name}' is not available. Thus, we cannot retrieve the server description. Error: {e.Message}");
}
//
// Append the ERI data source to the list. Use the server description if available:
//
if (string.IsNullOrWhiteSpace(eriServerDescription))
sb.AppendLine($"- Id={ds.Id}, name='{eriDataSource.Name}', type=external data source");
else
sb.AppendLine($"- Id={ds.Id}, name='{eriDataSource.Name}', type=external data source, description='{eriServerDescription}'");
break;
}
}
logger.LogInformation("Prepared the list of allowed data sources for the agent.");
additionalData.Add("availableDataSources", sb.ToString());
//
// 4. Let the agent select the data sources:
//
var prompt = $"""
The system prompt is:
```
{chatThread.SystemPrompt}
```
The user prompt is:
```
{lastPromptContent}
```
""";
// Call the agent:
var aiResponse = await this.ProcessInput(new ContentBlock
{
Time = DateTimeOffset.UtcNow,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
{
Text = prompt,
},
}, additionalData);
if(aiResponse.Content is null)
{
logger.LogWarning("The agent did not return a response.");
return [];
}
switch (aiResponse)
{
//
// 5. Parse the agent response:
//
case { ContentType: ContentType.TEXT, Content: ContentText textContent }:
{
//
// What we expect is a JSON list of SelectedDataSource objects:
//
var selectedDataSourcesJson = textContent.Text;
//
// We know how bad LLM may be in generating JSON without surrounding text.
// Thus, we expect the worst and try to extract the JSON list from the text:
//
var json = ExtractJson(selectedDataSourcesJson);
try
{
var aiSelectedDataSources = JsonSerializer.Deserialize<List<SelectedDataSource>>(json, JSON_SERIALIZER_OPTIONS);
return aiSelectedDataSources ?? [];
}
catch
{
logger.LogWarning("The agent answered with an invalid or unexpected JSON format.");
return [];
}
}
case { ContentType: ContentType.TEXT }:
logger.LogWarning("The agent answered with an unexpected inner content type.");
return [];
case { ContentType: ContentType.NONE }:
logger.LogWarning("The agent did not return a response.");
return [];
default:
logger.LogWarning($"The agent answered with an unexpected content type '{aiResponse.ContentType}'.");
return [];
}
}
/// <summary>
/// Extracts the JSON list from the given text. The text may contain additional
/// information around the JSON list. The method tries to extract the JSON list
/// from the text.
/// </summary>
/// <remarks>
/// Algorithm: The method searches for the first line that contains only a '[' character.
/// Then, it searches for the first line that contains only a ']' character. The method
/// returns the text between these two lines (including the brackets). When the method
/// cannot find the JSON list, it returns an empty string.
/// </remarks>
/// <param name="text">The text that may contain the JSON list.</param>
/// <returns>The extracted JSON list.</returns>
private static ReadOnlySpan<char> ExtractJson(ReadOnlySpan<char> text)
{
var startIndex = -1;
var endIndex = -1;
var foundStart = false;
var foundEnd = false;
var lineStart = 0;
for (var i = 0; i <= text.Length; i++)
{
// Handle the end of the line or the end of the text:
if (i == text.Length || text[i] == '\n')
{
if (IsCharacterAloneInLine(text, lineStart, i, '[') && !foundStart)
{
startIndex = lineStart;
foundStart = true;
}
else if (IsCharacterAloneInLine(text, lineStart, i, ']') && foundStart && !foundEnd)
{
endIndex = i;
foundEnd = true;
break;
}
lineStart = i + 1;
}
}
if (foundStart && foundEnd)
{
// Adjust endIndex for slicing, ensuring it's within bounds:
return text.Slice(startIndex, Math.Min(text.Length, endIndex + 1) - startIndex);
}
return ReadOnlySpan<char>.Empty;
}
private static bool IsCharacterAloneInLine(ReadOnlySpan<char> text, int lineStart, int lineEnd, char character)
{
for (var i = lineStart; i < lineEnd; i++)
if (!char.IsWhiteSpace(text[i]) && text[i] != character)
return false;
return true;
}
}

View File

@ -0,0 +1,385 @@
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.RAG;
using AIStudio.Tools.Services;
namespace AIStudio.Agents;
public sealed class AgentRetrievalContextValidation (ILogger<AgentRetrievalContextValidation> logger, ILogger<AgentBase> baseLogger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : AgentBase(baseLogger, settingsManager, dataSourceService, rng)
{
#region Overrides of AgentBase
/// <inheritdoc />
protected override Type Type => Type.WORKER;
/// <inheritdoc />
public override string Id => "Retrieval Context Validation";
/// <inheritdoc />
protected override string JobDescription =>
"""
You receive a system and user prompt as well as a retrieval context as input. Your task is to decide whether this
retrieval context is helpful in processing the prompts or not. You respond with the decision (true or false),
your reasoning, and your confidence in this decision.
Your response is only one JSON object in the following format:
```
{"decision": true, "reason": "Why did you choose this source?", "confidence": 0.87}
```
You express your confidence as a floating-point number between 0.0 (maximum uncertainty) and
1.0 (you are absolutely certain that this retrieval context is needed).
The JSON schema is:
```
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"decision": {
"type": "boolean"
},
"reason": {
"type": "string"
},
"confidence": {
"type": "number"
}
},
"required": [
"decision",
"reason",
"confidence"
]
}
```
You do not ask any follow-up questions. You do not address the user. Your response consists solely of
that one JSON object.
""";
/// <inheritdoc />
protected override string SystemPrompt(string retrievalContext) => $"""
{this.JobDescription}
{retrievalContext}
""";
/// <inheritdoc />
public override Settings.Provider? ProviderSettings { get; set; }
/// <summary>
/// The retrieval context validation agent does not work with context. Use
/// the process input method instead.
/// </summary>
/// <returns>The chat thread without any changes.</returns>
public override Task<ChatThread> ProcessContext(ChatThread chatThread, IDictionary<string, string> additionalData) => Task.FromResult(chatThread);
/// <inheritdoc />
public override async Task<ContentBlock> ProcessInput(ContentBlock input, IDictionary<string, string> additionalData)
{
if (input.Content is not ContentText text)
return EMPTY_BLOCK;
if(text.InitialRemoteWait || text.IsStreaming)
return EMPTY_BLOCK;
if(string.IsNullOrWhiteSpace(text.Text))
return EMPTY_BLOCK;
if(!additionalData.TryGetValue("retrievalContext", out var retrievalContext) || string.IsNullOrWhiteSpace(retrievalContext))
return EMPTY_BLOCK;
var thread = this.CreateChatThread(this.SystemPrompt(retrievalContext));
var userRequest = this.AddUserRequest(thread, text.Text);
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
return thread.Blocks[^1];
}
/// <inheritdoc />
public override Task<bool> MadeDecision(ContentBlock input) => Task.FromResult(true);
/// <summary>
/// We do not provide any context. This agent will process many retrieval contexts.
/// This would block a huge amount of memory.
/// </summary>
/// <returns>An empty list.</returns>
public override IReadOnlyCollection<ContentBlock> GetContext() => [];
/// <summary>
/// We do not provide any answers. This agent will process many retrieval contexts.
/// This would block a huge amount of memory.
/// </summary>
/// <returns>An empty list.</returns>
public override IReadOnlyCollection<ContentBlock> GetAnswers() => [];
#endregion
/// <summary>
/// Sets the LLM provider for the agent.
/// </summary>
/// <remarks>
/// When you have to call the validation in parallel for many retrieval contexts,
/// you can set the provider once and then call the validation method in parallel.
/// </remarks>
/// <param name="provider">The current LLM provider. When the user doesn't preselect an agent provider, the agent uses this provider.</param>
public void SetLLMProvider(IProvider provider)
{
// We start with the provider currently selected by the user:
var agentProvider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION, provider.Id, true);
// Assign the provider settings to the agent:
logger.LogInformation($"The agent for the retrieval context validation uses the provider '{agentProvider.InstanceName}' ({agentProvider.UsedLLMProvider.ToName()}, confidence={agentProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level.GetName()}).");
this.ProviderSettings = agentProvider;
}
/// <summary>
/// Validate all retrieval contexts against the last user and the system prompt.
/// </summary>
/// <param name="lastPrompt">The last user prompt.</param>
/// <param name="chatThread">The chat thread.</param>
/// <param name="retrievalContexts">All retrieval contexts to validate.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The validation results.</returns>
public async Task<IReadOnlyList<RetrievalContextValidationResult>> ValidateRetrievalContextsAsync(IContent lastPrompt, ChatThread chatThread, IReadOnlyList<IRetrievalContext> retrievalContexts, CancellationToken token = default)
{
// Check if the retrieval context validation is enabled:
if (!this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
return [];
logger.LogInformation($"Validating {retrievalContexts.Count:###,###,###,###} retrieval contexts.");
// Prepare the list of validation tasks:
var validationTasks = new List<Task<RetrievalContextValidationResult>>(retrievalContexts.Count);
// Read the number of parallel validations:
var numParallelValidations = 3;
if(this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions)
numParallelValidations = this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.NumParallelValidations;
numParallelValidations = Math.Max(1, numParallelValidations);
// Use a semaphore to limit the number of parallel validations:
using var semaphore = new SemaphoreSlim(numParallelValidations);
foreach (var retrievalContext in retrievalContexts)
{
// Wait for an available slot in the semaphore:
await semaphore.WaitAsync(token);
// Start the next validation task:
validationTasks.Add(this.ValidateRetrievalContextAsync(lastPrompt, chatThread, retrievalContext, token, semaphore));
}
// Wait for all validation tasks to complete:
return await Task.WhenAll(validationTasks);
}
/// <summary>
/// Validates the retrieval context against the last user and the system prompt.
/// </summary>
/// <remarks>
/// Probably, you have a lot of retrieval contexts to validate. In this case, you
/// can call this method in parallel for each retrieval context. You might use
/// the ValidateRetrievalContextsAsync method to validate all retrieval contexts.
/// </remarks>
/// <param name="lastPrompt">The last user prompt.</param>
/// <param name="chatThread">The chat thread.</param>
/// <param name="retrievalContext">The retrieval context to validate.</param>
/// <param name="token">The cancellation token.</param>
/// <param name="semaphore">The optional semaphore to limit the number of parallel validations.</param>
/// <returns>The validation result.</returns>
public async Task<RetrievalContextValidationResult> ValidateRetrievalContextAsync(IContent lastPrompt, ChatThread chatThread, IRetrievalContext retrievalContext, CancellationToken token = default, SemaphoreSlim? semaphore = null)
{
try
{
//
// Check if the validation was canceled. This could happen when the user
// canceled the validation process or when the validation process took
// too long:
//
if(token.IsCancellationRequested)
return new(false, "The validation was canceled.", 1.0f, retrievalContext);
//
// 1. Prepare the current system and user prompts as input for the agent:
//
var lastPromptContent = lastPrompt switch
{
ContentText text => text.Text,
// Image prompts may be empty, e.g., when the image is too large:
ContentImage image => await image.AsBase64(token),
// Other content types are not supported yet:
_ => string.Empty,
};
if (string.IsNullOrWhiteSpace(lastPromptContent))
{
logger.LogWarning("The last prompt is empty. The AI cannot validate the retrieval context.");
return new(false, "The last prompt was empty.", 1.0f, retrievalContext);
}
//
// 2. Prepare the retrieval context for the agent:
//
var additionalData = new Dictionary<string, string>();
var markdownRetrievalContext = await retrievalContext.AsMarkdown(token: token);
additionalData.Add("retrievalContext", markdownRetrievalContext);
//
// 3. Let the agent validate the retrieval context:
//
var prompt = $"""
The system prompt is:
```
{chatThread.SystemPrompt}
```
The user prompt is:
```
{lastPromptContent}
```
""";
// Call the agent:
var aiResponse = await this.ProcessInput(new ContentBlock
{
Time = DateTimeOffset.UtcNow,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
{
Text = prompt,
},
}, additionalData);
if (aiResponse.Content is null)
{
logger.LogWarning("The agent did not return a response.");
return new(false, "The agent did not return a response.", 1.0f, retrievalContext);
}
switch (aiResponse)
{
//
// 4. Parse the agent response:
//
case { ContentType: ContentType.TEXT, Content: ContentText textContent }:
{
//
// What we expect is one JSON object:
//
var validationJson = textContent.Text;
//
// We know how bad LLM may be in generating JSON without surrounding text.
// Thus, we expect the worst and try to extract the JSON list from the text:
//
var json = ExtractJson(validationJson);
try
{
var result = JsonSerializer.Deserialize<RetrievalContextValidationResult>(json, JSON_SERIALIZER_OPTIONS);
return result with { RetrievalContext = retrievalContext };
}
catch
{
logger.LogWarning("The agent answered with an invalid or unexpected JSON format.");
return new(false, "The agent answered with an invalid or unexpected JSON format.", 1.0f, retrievalContext);
}
}
case { ContentType: ContentType.TEXT }:
logger.LogWarning("The agent answered with an unexpected inner content type.");
return new(false, "The agent answered with an unexpected inner content type.", 1.0f, retrievalContext);
case { ContentType: ContentType.NONE }:
logger.LogWarning("The agent did not return a response.");
return new(false, "The agent did not return a response.", 1.0f, retrievalContext);
default:
logger.LogWarning($"The agent answered with an unexpected content type '{aiResponse.ContentType}'.");
return new(false, $"The agent answered with an unexpected content type '{aiResponse.ContentType}'.", 1.0f, retrievalContext);
}
}
finally
{
// Release the semaphore slot:
semaphore?.Release();
}
}
private static ReadOnlySpan<char> ExtractJson(ReadOnlySpan<char> input)
{
//
// 1. Expect the best case ;-)
//
if (CheckJsonObjectStart(input))
return ExtractJsonPart(input);
//
// 2. Okay, we have some garbage before the
// JSON object. We expected that...
//
for (var index = 0; index < input.Length; index++)
{
if (input[index] is '{' && CheckJsonObjectStart(input[index..]))
return ExtractJsonPart(input[index..]);
}
return [];
}
private static bool CheckJsonObjectStart(ReadOnlySpan<char> area)
{
char[] expectedSymbols = ['{', '"', 'd'];
var symbolIndex = 0;
foreach (var c in area)
{
if (symbolIndex >= expectedSymbols.Length)
return true;
if (char.IsWhiteSpace(c))
continue;
if (c == expectedSymbols[symbolIndex++])
continue;
return false;
}
return true;
}
private static ReadOnlySpan<char> ExtractJsonPart(ReadOnlySpan<char> input)
{
var insideString = false;
for (var index = 0; index < input.Length; index++)
{
if (input[index] is '"')
{
insideString = !insideString;
continue;
}
if (insideString)
continue;
if (input[index] is '}')
return input[..++index];
}
return [];
}
}

View File

@ -1,18 +1,11 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Services;
namespace AIStudio.Agents; namespace AIStudio.Agents;
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, rng) public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, DataSourceService dataSourceService, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, dataSourceService, rng)
{ {
private static readonly ContentBlock EMPTY_BLOCK = new()
{
Content = null,
ContentType = ContentType.NONE,
Role = ChatRole.AGENT,
Time = DateTimeOffset.UtcNow,
};
private readonly List<ContentBlock> context = new(); private readonly List<ContentBlock> context = new();
private readonly List<ContentBlock> answers = new(); private readonly List<ContentBlock> answers = new();
@ -72,8 +65,8 @@ public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsM
return EMPTY_BLOCK; return EMPTY_BLOCK;
var thread = this.CreateChatThread(this.SystemPrompt(sourceURL)); var thread = this.CreateChatThread(this.SystemPrompt(sourceURL));
var time = this.AddUserRequest(thread, text.Text); var userRequest = this.AddUserRequest(thread, text.Text);
await this.AddAIResponseAsync(thread, time); await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
var answer = thread.Blocks[^1]; var answer = thread.Blocks[^1];
this.answers.Add(answer); this.answers.Add(answer);

View File

@ -0,0 +1,12 @@
using AIStudio.Tools.RAG;
namespace AIStudio.Agents;
/// <summary>
/// Represents the result of a retrieval context validation.
/// </summary>
/// <param name="Decision">Whether the retrieval context is useful or not.</param>
/// <param name="Reason">The reason for the decision.</param>
/// <param name="Confidence">The confidence of the decision.</param>
/// <param name="RetrievalContext">The retrieval context that was validated.</param>
public readonly record struct RetrievalContextValidationResult(bool Decision, string Reason, float Confidence, IRetrievalContext? RetrievalContext) : IConfidence;

View File

@ -0,0 +1,9 @@
namespace AIStudio.Agents;
/// <summary>
/// Represents a selected data source, chosen by the agent.
/// </summary>
/// <param name="Id">The data source ID.</param>
/// <param name="Reason">The reason for selecting the data source.</param>
/// <param name="Confidence">The confidence of the agent in the selection.</param>
public readonly record struct SelectedDataSource(string Id, string Reason, float Confidence) : IConfidence;

View File

@ -0,0 +1,19 @@
using AIStudio.Chat;
namespace AIStudio.Agents;
/// <summary>
/// The created user request.
/// </summary>
public sealed class UserRequest
{
/// <summary>
/// The time when the request was created.
/// </summary>
public required DateTimeOffset Time { get; init; }
/// <summary>
/// The user prompt.
/// </summary>
public required IContent UserPrompt { get; init; }
}

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_AGENDA)] @attribute [Route(Routes.ASSISTANT_AGENDA)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogAgenda>
<MudTextField T="string" @bind-Text="@this.inputName" Validation="@this.ValidateName" Label="Meeting Name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Tag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Name the meeting, seminar, etc." Placeholder="Weekly jour fixe" Class="mb-3"/> <MudTextField T="string" @bind-Text="@this.inputName" Validation="@this.ValidateName" Label="Meeting Name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Tag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Name the meeting, seminar, etc." Placeholder="Weekly jour fixe" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputTopic" Validation="@this.ValidateTopic" Label="Topic" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.EventNote" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe the topic of the meeting, seminar, etc. Is it about quantum computing, software engineering, or is it a general business meeting?" Placeholder="Project meeting" Class="mb-3"/> <MudTextField T="string" @bind-Text="@this.inputTopic" Validation="@this.ValidateTopic" Label="Topic" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.EventNote" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" HelperText="Describe the topic of the meeting, seminar, etc. Is it about quantum computing, software engineering, or is it a general business meeting?" Placeholder="Project meeting" Class="mb-3"/>

View File

@ -1,10 +1,11 @@
using System.Text; using System.Text;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Agenda; namespace AIStudio.Assistants.Agenda;
public partial class AssistantAgenda : AssistantBaseCore public partial class AssistantAgenda : AssistantBaseCore<SettingsDialogAgenda>
{ {
public override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT; public override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT;

View File

@ -1,131 +1,151 @@
@using AIStudio.Chat @using AIStudio.Chat
@inherits AssistantLowerBase
@typeparam TSettings
<MudText Typo="Typo.h3" Class="mb-2 mr-3"> <div class="inner-scrolling-context">
@(this.Title)
</MudText>
<InnerScrolling HeaderHeight="6em"> <MudText Typo="Typo.h3" Class="mb-2 mr-3">
<ChildContent> @(this.Title)
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2"> </MudText>
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
@this.Description
</MudText>
@if (this.Body is not null) <InnerScrolling>
{ <ChildContent>
<CascadingValue Value="@this"> <MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2">
@this.Body <MudGrid Class="mb-2">
</CascadingValue> <MudItem xs="10">
<MudText Typo="Typo.body1" Align="Align.Justify">
@this.Description
</MudText>
</MudItem>
<MudItem xs="2" Class="d-flex justify-end align-start">
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Settings" OnClick="() => this.OpenSettingsDialog()"/>
</MudItem>
</MudGrid>
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle"> @if (this.Body is not null)
@this.SubmitText
</MudButton>
}
</MudForm>
<Issues IssuesData="@(this.inputIssues)"/>
@if (this.ShowDedicatedProgress && this.isProcessing)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
}
<div id="@RESULT_DIV_ID" class="mr-2 mt-3">
</div>
<div id="@BEFORE_RESULT_DIV_ID" class="mt-3">
</div>
@if (this.ShowResult && !this.ShowEntireChatThread && this.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.Content)"/>
}
@if(this.ShowResult && this.ShowEntireChatThread && this.chatThread is not null)
{
foreach (var block in this.chatThread.Blocks.OrderBy(n => n.Time))
{
@if (!block.HideFromUser)
{ {
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content"/> <CascadingValue Value="@this">
} @this.Body
} </CascadingValue>
}
<div id="@AFTER_RESULT_DIV_ID" class="mt-3"> <MudStack Row="true" AlignItems="AlignItems.Center" StretchItems="StretchItems.Start" Class="mb-3">
</div> <MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle">
</ChildContent> @this.SubmitText
<FooterContent>
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
@if (!this.FooterButtons.Any(x => x.Type is ButtonTypes.SEND_TO))
{
@if (this.ShowSendTo)
{
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
{
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
}
}
@foreach (var button in this.FooterButtons)
{
switch (button)
{
case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
<MudTooltip Text="@buttonData.Tooltip">
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
</MudTooltip>
break;
case ButtonData buttonData:
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton> </MudButton>
break; @if (this.isProcessing && this.cancellationTokenSource is not null)
{
<MudTooltip Text="Stop generation">
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="() => this.CancelStreaming()"/>
</MudTooltip>
}
</MudStack>
}
</MudForm>
<Issues IssuesData="@(this.inputIssues)"/>
case SendToButton sendToButton: @if (this.ShowDedicatedProgress && this.isProcessing)
<MudMenu StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded"> {
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
}
<div id="@RESULT_DIV_ID" class="mr-2 mt-3">
</div>
<div id="@BEFORE_RESULT_DIV_ID" class="mt-3">
</div>
@if (this.ShowResult && !this.ShowEntireChatThread && this.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.Content)"/>
}
@if(this.ShowResult && this.ShowEntireChatThread && this.chatThread is not null)
{
foreach (var block in this.chatThread.Blocks.OrderBy(n => n.Time))
{
@if (!block.HideFromUser)
{
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content"/>
}
}
}
<div id="@AFTER_RESULT_DIV_ID" class="mt-3">
</div>
</ChildContent>
<FooterContent>
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="ma-1">
@if (!this.FooterButtons.Any(x => x.Type is ButtonTypes.SEND_TO))
{
@if (this.ShowSendTo)
{
<MudMenu AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length)) @foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
{ {
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)"> <MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
@assistant.Name() @assistant.Name()
</MudMenuItem> </MudMenuItem>
} }
</MudMenu> </MudMenu>
break; }
} }
}
@if (this.ShowCopyResult) @foreach (var button in this.FooterButtons)
{ {
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()"> switch (button)
Copy result {
</MudButton> case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
} <MudTooltip Text="@buttonData.Tooltip">
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
</MudTooltip>
break;
@if (this.ShowReset) case ButtonData buttonData:
{ <MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
<MudButton Variant="Variant.Filled" Style="@this.GetResetColor()" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()"> @buttonData.Text
Reset </MudButton>
</MudButton> break;
}
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence) case SendToButton sendToButton:
{ <MudMenu AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" StartIcon="@Icons.Material.Filled.Apps" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="Send to ..." Variant="Variant.Filled" Style="@this.GetSendToColor()" Class="rounded">
<ConfidenceInfo Mode="ConfidenceInfoMode.BUTTON" LLMProvider="@this.providerSettings.UsedLLMProvider"/> @foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
} {
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
break;
}
}
@if (this.AllowProfiles && this.ShowProfileSelection) @if (this.ShowCopyResult)
{ {
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.currentProfile"/> <MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
} Copy result
</MudStack> </MudButton>
</FooterContent> }
</InnerScrolling>
@if (this.ShowReset)
{
<MudButton Variant="Variant.Filled" Style="@this.GetResetColor()" StartIcon="@Icons.Material.Filled.Refresh" OnClick="() => this.InnerResetForm()">
Reset
</MudButton>
}
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfidenceInfo Mode="PopoverTriggerMode.BUTTON" LLMProvider="@this.providerSettings.UsedLLMProvider"/>
}
@if (this.AllowProfiles && this.ShowProfileSelection)
{
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.currentProfile"/>
}
</MudStack>
</FooterContent>
</InnerScrolling>
</div>

View File

@ -1,21 +1,26 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor.Utilities; using MudBlazor.Utilities;
using RustService = AIStudio.Tools.RustService;
using Timer = System.Timers.Timer; using Timer = System.Timers.Timer;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Assistants; namespace AIStudio.Assistants;
public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver, IDisposable public abstract partial class AssistantBase<TSettings> : AssistantLowerBase, IMessageBusReceiver, IDisposable where TSettings : IComponent
{ {
[Inject] [Inject]
protected SettingsManager SettingsManager { get; init; } = null!; protected SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
[Inject] [Inject]
protected IJSRuntime JsRuntime { get; init; } = null!; protected IJSRuntime JsRuntime { get; init; } = null!;
@ -32,7 +37,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
protected NavigationManager NavigationManager { get; init; } = null!; protected NavigationManager NavigationManager { get; init; } = null!;
[Inject] [Inject]
protected ILogger<AssistantBase> Logger { get; init; } = null!; protected ILogger<AssistantBase<TSettings>> Logger { get; init; } = null!;
[Inject] [Inject]
private MudTheme ColorTheme { get; init; } = null!; private MudTheme ColorTheme { get; init; } = null!;
@ -40,10 +45,6 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
[Inject] [Inject]
private MessageBus MessageBus { get; init; } = null!; private MessageBus MessageBus { get; init; } = null!;
internal const string RESULT_DIV_ID = "assistantResult";
internal const string BEFORE_RESULT_DIV_ID = "beforeAssistantResult";
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
protected abstract string Title { get; } protected abstract string Title { get; }
protected abstract string Description { get; } protected abstract string Description { get; }
@ -90,16 +91,16 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
protected virtual IReadOnlyList<IButtonData> FooterButtons => []; protected virtual IReadOnlyList<IButtonData> FooterButtons => [];
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
protected AIStudio.Settings.Provider providerSettings; protected AIStudio.Settings.Provider providerSettings;
protected MudForm? form; protected MudForm? form;
protected bool inputIsValid; protected bool inputIsValid;
protected Profile currentProfile = Profile.NO_PROFILE; protected Profile currentProfile = Profile.NO_PROFILE;
protected ChatThread? chatThread; protected ChatThread? chatThread;
protected IContent? lastUserPrompt;
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6)); private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
private CancellationTokenSource? cancellationTokenSource;
private ContentBlock? resultingContentBlock; private ContentBlock? resultingContentBlock;
private string[] inputIssues = []; private string[] inputIssues = [];
private bool isProcessing; private bool isProcessing;
@ -147,6 +148,8 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(AssistantBase<TSettings>);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)
@ -240,16 +243,18 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false) protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false)
{ {
var time = DateTimeOffset.Now; var time = DateTimeOffset.Now;
this.lastUserPrompt = new ContentText
{
Text = request,
};
this.chatThread!.Blocks.Add(new ContentBlock this.chatThread!.Blocks.Add(new ContentBlock
{ {
Time = time, Time = time,
ContentType = ContentType.TEXT, ContentType = ContentType.TEXT,
HideFromUser = hideContentFromUser, HideFromUser = hideContentFromUser,
Role = ChatRole.USER, Role = ChatRole.USER,
Content = new ContentText Content = this.lastUserPrompt,
{
Text = request,
},
}); });
return time; return time;
@ -282,11 +287,15 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
this.isProcessing = true; this.isProcessing = true;
this.StateHasChanged(); this.StateHasChanged();
// Use the selected provider to get the AI response. using (this.cancellationTokenSource = new())
// By awaiting this line, we wait for the entire {
// content to be streamed. // Use the selected provider to get the AI response.
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread); // By awaiting this line, we wait for the entire
// content to be streamed.
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread, this.cancellationTokenSource.Token);
}
this.cancellationTokenSource = null;
this.isProcessing = false; this.isProcessing = false;
this.StateHasChanged(); this.StateHasChanged();
@ -294,6 +303,13 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
return aiText.Text; return aiText.Text;
} }
private async Task CancelStreaming()
{
if (this.cancellationTokenSource is not null)
if(!this.cancellationTokenSource.IsCancellationRequested)
await this.cancellationTokenSource.CancelAsync();
}
protected async Task CopyToClipboard() protected async Task CopyToClipboard()
{ {
await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy()); await this.RustService.CopyText2Clipboard(this.Snackbar, this.Result2Copy());
@ -307,6 +323,12 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
return icon; return icon;
} }
protected async Task OpenSettingsDialog()
{
var dialogParameters = new DialogParameters();
await this.DialogService.ShowAsync<TSettings>(null, dialogParameters, DialogOptions.FULLSCREEN);
}
protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton) protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
{ {
if (!destination.AllowSendTo()) if (!destination.AllowSendTo())

View File

@ -7,7 +7,7 @@ namespace AIStudio.Assistants;
// See https://stackoverflow.com/a/77300384/2258393 for why this class is necessary // See https://stackoverflow.com/a/77300384/2258393 for why this class is necessary
// //
public abstract class AssistantBaseCore : AssistantBase public abstract class AssistantBaseCore<TSettings> : AssistantBase<TSettings> where TSettings : IComponent
{ {
private protected sealed override RenderFragment Body => this.BuildRenderTree; private protected sealed override RenderFragment Body => this.BuildRenderTree;

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Assistants;
public abstract class AssistantLowerBase : ComponentBase
{
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
internal const string RESULT_DIV_ID = "assistantResult";
internal const string BEFORE_RESULT_DIV_ID = "beforeAssistantResult";
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
}

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_BIAS)] @attribute [Route(Routes.ASSISTANT_BIAS)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogAssistantBias>
<MudText Typo="Typo.body1"> <MudText Typo="Typo.body1">
<b>Links:</b> <b>Links:</b>

View File

@ -1,11 +1,12 @@
using System.Text; using System.Text;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
namespace AIStudio.Assistants.BiasDay; namespace AIStudio.Assistants.BiasDay;
public partial class BiasOfTheDayAssistant : AssistantBaseCore public partial class BiasOfTheDayAssistant : AssistantBaseCore<SettingsDialogAssistantBias>
{ {
public override Tools.Components Component => Tools.Components.BIAS_DAY_ASSISTANT; public override Tools.Components Component => Tools.Components.BIAS_DAY_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_CODING)] @attribute [Route(Routes.ASSISTANT_CODING)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogCoding>
<MudExpansionPanels Class="mb-3"> <MudExpansionPanels Class="mb-3">
@for (var contextIndex = 0; contextIndex < this.codingContexts.Count; contextIndex++) @for (var contextIndex = 0; contextIndex < this.codingContexts.Count; contextIndex++)

View File

@ -1,8 +1,10 @@
using System.Text; using System.Text;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Coding; namespace AIStudio.Assistants.Coding;
public partial class AssistantCoding : AssistantBaseCore public partial class AssistantCoding : AssistantBaseCore<SettingsDialogCoding>
{ {
public override Tools.Components Component => Tools.Components.CODING_ASSISTANT; public override Tools.Components Component => Tools.Components.CODING_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_EMAIL)] @attribute [Route(Routes.ASSISTANT_EMAIL)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogWritingEMails>
<MudTextSwitch Label="Is there a history, a previous conversation?" @bind-Value="@this.provideHistory" LabelOn="Yes, I provide the previous conversation" LabelOff="No, I don't provide a previous conversation" /> <MudTextSwitch Label="Is there a history, a previous conversation?" @bind-Value="@this.provideHistory" LabelOn="Yes, I provide the previous conversation" LabelOff="No, I don't provide a previous conversation" />
@if (this.provideHistory) @if (this.provideHistory)

View File

@ -1,10 +1,11 @@
using System.Text; using System.Text;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.EMail; namespace AIStudio.Assistants.EMail;
public partial class AssistantEMail : AssistantBaseCore public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMails>
{ {
public override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT; public override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT;

View File

@ -2,15 +2,12 @@ namespace AIStudio.Assistants.ERI;
public static class AllowedLLMProvidersExtensions public static class AllowedLLMProvidersExtensions
{ {
public static string Description(this AllowedLLMProviders provider) public static string Description(this AllowedLLMProviders provider) => provider switch
{ {
return provider switch AllowedLLMProviders.NONE => "Please select what kind of LLM provider are allowed for this data source",
{ AllowedLLMProviders.ANY => "Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider",
AllowedLLMProviders.NONE => "Please select what kind of LLM provider are allowed for this data source", AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
AllowedLLMProviders.ANY => "Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider",
AllowedLLMProviders.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
_ => "Unknown option was selected" _ => "Unknown option was selected"
}; };
}
} }

View File

@ -1,14 +1,14 @@
@attribute [Route(Routes.ASSISTANT_ERI)] @attribute [Route(Routes.ASSISTANT_ERI)]
@using AIStudio.Settings.DataModel @using AIStudio.Settings.DataModel
@using MudExtensions @using MudExtensions
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogERIServer>
<MudText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize
all pages using an embedding method. All of Wikipedias data would remain with Wikipedia, including the all pages using an embedding method. All of Wikipedias data would remain with Wikipedia, including the
vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to vector database (decentralized approach). Then, any AI Studio user could add Wikipedia as a data source to
significantly reduce the hallucination of the LLM in knowledge questions. significantly reduce the hallucination of the LLM in knowledge questions.
</MudText> </MudJustifiedText>
<MudText Typo="Typo.body1"> <MudText Typo="Typo.body1">
<b>Related links:</b> <b>Related links:</b>
@ -25,10 +25,10 @@
ERI server presets ERI server presets
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
Here you have the option to save different configurations for various ERI servers and switch between them. This is useful if Here you have the option to save different configurations for various ERI servers and switch between them. This is useful if
you are responsible for multiple ERI servers. you are responsible for multiple ERI servers.
</MudText> </MudJustifiedText>
@if(this.SettingsManager.ConfigurationData.ERI.ERIServers.Count is 0) @if(this.SettingsManager.ConfigurationData.ERI.ERIServers.Count is 0)
{ {
@ -52,33 +52,33 @@ else
<MudButton Disabled="@this.AreServerPresetsBlocked" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary"> <MudButton Disabled="@this.AreServerPresetsBlocked" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary">
Add ERI server preset Add ERI server preset
</MudButton> </MudButton>
<MudButton OnClick="@this.RemoveERIServer" Disabled="@(this.AreServerPresetsBlocked || this.IsNoneERIServerSelected)" Variant="Variant.Filled" Color="Color.Primary"> <MudButton OnClick="@this.RemoveERIServer" Disabled="@(this.AreServerPresetsBlocked || this.IsNoneERIServerSelected)" Variant="Variant.Filled" Color="Color.Error">
Delete this server preset Delete this server preset
</MudButton> </MudButton>
</MudStack> </MudStack>
@if(this.AreServerPresetsBlocked) @if(this.AreServerPresetsBlocked)
{ {
<MudText Typo="Typo.body1" Class="mb-3 mt-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3 mt-3">
Hint: to allow this assistant to manage multiple presets, you must enable the preselection of values in the settings. Hint: to allow this assistant to manage multiple presets, you must enable the preselection of values in the settings.
</MudText> </MudJustifiedText>
} }
<MudText Typo="Typo.h4" Class="mb-3 mt-6"> <MudText Typo="Typo.h4" Class="mb-3 mt-6">
Auto save Auto save
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
The ERI specification will change over time. You probably want to keep your ERI server up to date. This means you might want to The ERI specification will change over time. You probably want to keep your ERI server up to date. This means you might want to
regenerate the code for your ERI server. To avoid having to make all inputs each time, all your inputs and decisions can be regenerate the code for your ERI server. To avoid having to make all inputs each time, all your inputs and decisions can be
automatically saved. Would you like this? automatically saved. Would you like this?
</MudText> </MudJustifiedText>
@if(this.AreServerPresetsBlocked) @if(this.AreServerPresetsBlocked)
{ {
<MudText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
Hint: to allow this assistant to automatically save your changes, you must enable the preselection of values in the settings. Hint: to allow this assistant to automatically save your changes, you must enable the preselection of values in the settings.
</MudText> </MudJustifiedText>
} }
<MudTextSwitch Label="Should we automatically save any input made?" Disabled="@this.AreServerPresetsBlocked" @bind-Value="@this.autoSave" LabelOn="Yes, please save my inputs" LabelOff="No, I will enter everything again or configure it manually in the settings" /> <MudTextSwitch Label="Should we automatically save any input made?" Disabled="@this.AreServerPresetsBlocked" @bind-Value="@this.autoSave" LabelOn="Yes, please save my inputs" LabelOff="No, I will enter everything again or configure it manually in the settings" />
@ -204,18 +204,18 @@ else
Embedding settings Embedding settings
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-2"> <MudJustifiedText Typo="Typo.body1" Class="mb-2">
You will likely use one or more embedding methods to encode the meaning of your data into a typically high-dimensional vector You will likely use one or more embedding methods to encode the meaning of your data into a typically high-dimensional vector
space. In this case, you will use a vector database to store and search these vectors (called embeddings). However, you don't space. In this case, you will use a vector database to store and search these vectors (called embeddings). However, you don't
have to use embedding methods. When your retrieval method works without any embedding, you can ignore this section. An example: You have to use embedding methods. When your retrieval method works without any embedding, you can ignore this section. An example: You
store files on a file server, and your retrieval method works exclusively with file names in the file system, so you don't store files on a file server, and your retrieval method works exclusively with file names in the file system, so you don't
need embeddings. need embeddings.
</MudText> </MudJustifiedText>
<MudText Typo="Typo.body1" Class="mb-3"> <MudJustifiedText Typo="Typo.body1" Class="mb-3">
You can specify more than one embedding method. This can be useful when you want to use different embeddings for different queries You can specify more than one embedding method. This can be useful when you want to use different embeddings for different queries
or data types. For example, one embedding for texts, another for images, and a third for videos, etc. or data types. For example, one embedding for texts, another for images, and a third for videos, etc.
</MudText> </MudJustifiedText>
@if (!this.IsNoneERIServerSelected) @if (!this.IsNoneERIServerSelected)
{ {
@ -228,18 +228,20 @@ else
<HeaderContent> <HeaderContent>
<MudTh>Name</MudTh> <MudTh>Name</MudTh>
<MudTh>Type</MudTh> <MudTh>Type</MudTh>
<MudTh Style="text-align: left;">Actions</MudTh> <MudTh>Actions</MudTh>
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd>@context.EmbeddingName</MudTd> <MudTd>@context.EmbeddingName</MudTd>
<MudTd>@context.EmbeddingType</MudTd> <MudTd>@context.EmbeddingType</MudTd>
<MudTd Style="text-align: left;"> <MudTd>
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditEmbedding(context)"> <MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
Edit <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditEmbedding(context)">
</MudButton> Edit
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteEmbedding(context)"> </MudButton>
Delete <MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteEmbedding(context)">
</MudButton> Delete
</MudButton>
</MudStack>
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>
</MudTable> </MudTable>
@ -258,12 +260,12 @@ else
Data retrieval settings Data retrieval settings
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-2"> <MudJustifiedText Typo="Typo.body1" Class="mb-2">
For your ERI server, you need to retrieve data that matches a chat or prompt in some way. We call this the retrieval process. For your ERI server, you need to retrieve data that matches a chat or prompt in some way. We call this the retrieval process.
You must describe at least one such process. You may offer several retrieval processes from which users can choose. This allows You must describe at least one such process. You may offer several retrieval processes from which users can choose. This allows
you to test with beta users which process works better. Or you might generally want to give users the choice so they can select you to test with beta users which process works better. Or you might generally want to give users the choice so they can select
the process that best suits their circumstances. the process that best suits their circumstances.
</MudText> </MudJustifiedText>
@if (!this.IsNoneERIServerSelected) @if (!this.IsNoneERIServerSelected)
{ {
@ -274,17 +276,19 @@ else
</ColGroup> </ColGroup>
<HeaderContent> <HeaderContent>
<MudTh>Name</MudTh> <MudTh>Name</MudTh>
<MudTh Style="text-align: left;">Actions</MudTh> <MudTh>Actions</MudTh>
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd>@context.Name</MudTd> <MudTd>@context.Name</MudTd>
<MudTd Style="text-align: left;"> <MudTd>
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditRetrievalProcess(context)"> <MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
Edit <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditRetrievalProcess(context)">
</MudButton> Edit
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteRetrievalProcess(context)"> </MudButton>
Delete <MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteRetrievalProcess(context)">
</MudButton> Delete
</MudButton>
</MudStack>
</MudTd> </MudTd>
</RowTemplate> </RowTemplate>
</MudTable> </MudTable>
@ -299,13 +303,13 @@ else
Add Retrieval Process Add Retrieval Process
</MudButton> </MudButton>
<MudText Typo="Typo.body1" Class="mb-1"> <MudJustifiedText Typo="Typo.body1" Class="mb-1">
You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text You can integrate additional libraries. Perhaps you want to evaluate the prompts in advance using a machine learning method or analyze them with a text
mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here. mining approach? Or maybe you want to preprocess images in the prompts? For such advanced scenarios, you can specify which libraries you want to use here.
It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these It's best to describe which library you want to integrate for which purpose. This way, the LLM that writes the ERI server for you can try to use these
libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe libraries effectively. This should result in less rework being necessary. If you don't know the necessary libraries, you can instead attempt to describe
the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected. the intended use. The LLM can then attempt to choose suitable libraries. However, hallucinations can occur, and fictional libraries might be selected.
</MudText> </MudJustifiedText>
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.additionalLibraries" Label="(Optional) Additional libraries" HelperText="Do you want to include additional libraries? Then name them and briefly describe what you want to achieve with them." Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="12" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/> <MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.additionalLibraries" Label="(Optional) Additional libraries" HelperText="Do you want to include additional libraries? Then name them and briefly describe what you want to achieve with them." Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="12" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
@ -313,17 +317,17 @@ else
Provider selection for generation Provider selection for generation
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-2"> <MudJustifiedText Typo="Typo.body1" Class="mb-2">
The task of writing the ERI server for you is very complex. Therefore, a very powerful LLM is needed to successfully accomplish this task. The task of writing the ERI server for you is very complex. Therefore, a very powerful LLM is needed to successfully accomplish this task.
Small local models will probably not be sufficient. Instead, try using a large cloud-based or a large self-hosted model. Small local models will probably not be sufficient. Instead, try using a large cloud-based or a large self-hosted model.
</MudText> </MudJustifiedText>
<MudText Typo="Typo.body1" Class="mb-2"> <MudJustifiedText Typo="Typo.body1" Class="mb-2">
<b>Important:</b> The LLM may need to generate many files. This reaches the request limit of most providers. Typically, only a certain number <b>Important:</b> The LLM may need to generate many files. This reaches the request limit of most providers. Typically, only a certain number
of requests can be made per minute, and only a maximum number of tokens can be generated per minute. AI Studio automatically considers this. of requests can be made per minute, and only a maximum number of tokens can be generated per minute. AI Studio automatically considers this.
<b>However, generating all the files takes a certain amount of time.</b> Local or self-hosted models may work without these limitations <b>However, generating all the files takes a certain amount of time.</b> Local or self-hosted models may work without these limitations
and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing. and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing.
</MudText> </MudJustifiedText>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/> <ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@ -331,19 +335,19 @@ else
Write code to file system Write code to file system
</MudText> </MudText>
<MudText Typo="Typo.body1" Class="mb-2"> <MudJustifiedText Typo="Typo.body1" Class="mb-2">
AI Studio can save the generated code to the file system. You can select a base folder for this. AI Studio ensures that no files are created AI Studio can save the generated code to the file system. You can select a base folder for this. AI Studio ensures that no files are created
outside of this base folder. Furthermore, we recommend that you create a Git repository in this folder. This way, you can see what changes the outside of this base folder. Furthermore, we recommend that you create a Git repository in this folder. This way, you can see what changes the
AI has made in which files. AI has made in which files.
</MudText> </MudJustifiedText>
<MudText Typo="Typo.body1" Class="mb-2"> <MudJustifiedText Typo="Typo.body1" Class="mb-2">
When you rebuild / re-generate the ERI server code, AI Studio proceeds as follows: All files generated last time will be deleted. All When you rebuild / re-generate the ERI server code, AI Studio proceeds as follows: All files generated last time will be deleted. All
other files you have created remain. Then, the AI generates the new files. <b>But beware:</b> It may happen that the AI generates a other files you have created remain. Then, the AI generates the new files. <b>But beware:</b> It may happen that the AI generates a
file this time that you manually created last time. In this case, your manually created file will then be overwritten. Therefore, file this time that you manually created last time. In this case, your manually created file will then be overwritten. Therefore,
you should always create a Git repository and commit or revert all changes before using this assistant. With a diff visualization, you should always create a Git repository and commit or revert all changes before using this assistant. With a diff visualization,
you can immediately see where the AI has made changes. It is best to use an IDE suitable for your selected language for this purpose. you can immediately see where the AI has made changes. It is best to use an IDE suitable for your selected language for this purpose.
</MudText> </MudJustifiedText>
<MudTextSwitch Label="Should we write the generated code to the file system?" Disabled="@this.IsNoneERIServerSelected" @bind-Value="@this.writeToFilesystem" LabelOn="Yes, please write or update all generated code to the file system" LabelOff="No, just show me the code" /> <MudTextSwitch Label="Should we write the generated code to the file system?" Disabled="@this.IsNoneERIServerSelected" @bind-Value="@this.writeToFilesystem" LabelOn="Yes, please write or update all generated code to the file system" LabelOff="No, just show me the code" />
<SelectDirectory Label="Base directory where to write the code" @bind-Directory="@this.baseDirectory" Disabled="@(this.IsNoneERIServerSelected || !this.writeToFilesystem)" DirectoryDialogTitle="Select the target directory for the ERI server"/> <SelectDirectory Label="Base directory where to write the code" @bind-Directory="@this.baseDirectory" Disabled="@(this.IsNoneERIServerSelected || !this.writeToFilesystem)" DirectoryDialogTitle="Select the target directory for the ERI server" Validation="@this.ValidateDirectory" />

View File

@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs; using AIStudio.Dialogs;
using AIStudio.Dialogs.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -11,7 +12,7 @@ using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Assistants.ERI; namespace AIStudio.Assistants.ERI;
public partial class AssistantERI : AssistantBaseCore public partial class AssistantERI : AssistantBaseCore<SettingsDialogERIServer>
{ {
[Inject] [Inject]
private HttpClient HttpClient { get; set; } = null!; private HttpClient HttpClient { get; set; } = null!;
@ -290,6 +291,7 @@ public partial class AssistantERI : AssistantBaseCore
- You consider the security of the implementation by applying the Security by Design principle. - You consider the security of the implementation by applying the Security by Design principle.
- Your output is formatted as Markdown. Code is formatted as code blocks. For every file, you - Your output is formatted as Markdown. Code is formatted as code blocks. For every file, you
create a separate code block with its file path and name as chapter title. create a separate code block with its file path and name as chapter title.
- Important: The JSON objects of the API messages use camel case for the data field names.
"""); """);
return sb.ToString(); return sb.ToString();
@ -300,6 +302,8 @@ public partial class AssistantERI : AssistantBaseCore
protected override bool ShowEntireChatThread => true; protected override bool ShowEntireChatThread => true;
protected override bool ShowSendTo => false;
protected override string SubmitText => "Create the ERI server"; protected override string SubmitText => "Create the ERI server";
protected override Func<Task> SubmitAction => this.GenerateServer; protected override Func<Task> SubmitAction => this.GenerateServer;
@ -476,6 +480,16 @@ public partial class AssistantERI : AssistantBaseCore
if(this.selectedERIServer is null) if(this.selectedERIServer is null)
return; return;
var dialogParameters = new DialogParameters
{
{ "Message", $"Are you sure you want to delete the ERI server preset '{this.selectedERIServer.ServerName}'?" },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete ERI server preset", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
this.SettingsManager.ConfigurationData.ERI.ERIServers.Remove(this.selectedERIServer); this.SettingsManager.ConfigurationData.ERI.ERIServers.Remove(this.selectedERIServer);
this.selectedERIServer = null; this.selectedERIServer = null;
this.ResetForm(); this.ResetForm();
@ -740,6 +754,17 @@ public partial class AssistantERI : AssistantBaseCore
return null; return null;
} }
private string? ValidateDirectory(string path)
{
if(!this.writeToFilesystem)
return null;
if(string.IsNullOrWhiteSpace(path))
return "Please provide a base directory for the ERI server to write files to.";
return null;
}
private string GetMultiSelectionAuthText(List<Auth> selectedValues) private string GetMultiSelectionAuthText(List<Auth> selectedValues)
{ {
if(selectedValues.Count == 0) if(selectedValues.Count == 0)

View File

@ -7,7 +7,7 @@ public static class ERIVersionExtensions
try try
{ {
var url = version.SpecificationURL(); var url = version.SpecificationURL();
var response = await httpClient.GetAsync(url); using var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync(); return await response.Content.ReadAsStringAsync();
} }
catch catch

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_GRAMMAR_SPELLING)] @attribute [Route(Routes.ASSISTANT_GRAMMAR_SPELLING)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogGrammarSpelling>
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to check" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to check" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />

View File

@ -1,8 +1,9 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.GrammarSpelling; namespace AIStudio.Assistants.GrammarSpelling;
public partial class AssistantGrammarSpelling : AssistantBaseCore public partial class AssistantGrammarSpelling : AssistantBaseCore<SettingsDialogGrammarSpelling>
{ {
public override Tools.Components Component => Tools.Components.GRAMMAR_SPELLING_ASSISTANT; public override Tools.Components Component => Tools.Components.GRAMMAR_SPELLING_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_ICON_FINDER)] @attribute [Route(Routes.ASSISTANT_ICON_FINDER)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogIconFinder>
<MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Label="Your context" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputContext" Validation="@this.ValidatingContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Label="Your context" Variant="Variant.Outlined" Lines="3" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>

View File

@ -1,6 +1,8 @@
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.IconFinder; namespace AIStudio.Assistants.IconFinder;
public partial class AssistantIconFinder : AssistantBaseCore public partial class AssistantIconFinder : AssistantBaseCore<SettingsDialogIconFinder>
{ {
public override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT; public override Tools.Components Component => Tools.Components.ICON_FINDER_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_JOB_POSTING)] @attribute [Route(Routes.ASSISTANT_JOB_POSTING)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogJobPostings>
<MudTextField T="string" @bind-Text="@this.inputCompanyName" Label="(Optional) The company name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Warehouse" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/> <MudTextField T="string" @bind-Text="@this.inputCompanyName" Label="(Optional) The company name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Warehouse" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudTextField T="string" @bind-Text="@this.inputCountryLegalFramework" Label="Provide the country, where the company is located" Validation="@this.ValidateCountryLegalFramework" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Flag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" HelperText="This is important to consider the legal framework of the country."/> <MudTextField T="string" @bind-Text="@this.inputCountryLegalFramework" Label="Provide the country, where the company is located" Validation="@this.ValidateCountryLegalFramework" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Flag" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" HelperText="This is important to consider the legal framework of the country."/>

View File

@ -1,8 +1,9 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.JobPosting; namespace AIStudio.Assistants.JobPosting;
public partial class AssistantJobPostings : AssistantBaseCore public partial class AssistantJobPostings : AssistantBaseCore<SettingsDialogJobPostings>
{ {
public override Tools.Components Component => Tools.Components.JOB_POSTING_ASSISTANT; public override Tools.Components Component => Tools.Components.JOB_POSTING_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_LEGAL_CHECK)] @attribute [Route(Routes.ASSISTANT_LEGAL_CHECK)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogLegalCheck>
@if (!this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader) @if (!this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)
{ {

View File

@ -1,8 +1,9 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.LegalCheck; namespace AIStudio.Assistants.LegalCheck;
public partial class AssistantLegalCheck : AssistantBaseCore public partial class AssistantLegalCheck : AssistantBaseCore<SettingsDialogLegalCheck>
{ {
public override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT; public override Tools.Components Component => Tools.Components.LEGAL_CHECK_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_MY_TASKS)] @attribute [Route(Routes.ASSISTANT_MY_TASKS)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogMyTasks>
<ProfileFormSelection Validation="@this.ValidateProfile" @bind-Profile="@this.currentProfile"/> <ProfileFormSelection Validation="@this.ValidateProfile" @bind-Profile="@this.currentProfile"/>
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Text or email" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Text or email" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>

View File

@ -1,9 +1,10 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
using AIStudio.Settings; using AIStudio.Settings;
namespace AIStudio.Assistants.MyTasks; namespace AIStudio.Assistants.MyTasks;
public partial class AssistantMyTasks : AssistantBaseCore public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
{ {
public override Tools.Components Component => Tools.Components.MY_TASKS_ASSISTANT; public override Tools.Components Component => Tools.Components.MY_TASKS_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_REWRITE)] @attribute [Route(Routes.ASSISTANT_REWRITE)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogRewrite>
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to improve" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to improve" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" /> <EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />

View File

@ -1,8 +1,9 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.RewriteImprove; namespace AIStudio.Assistants.RewriteImprove;
public partial class AssistantRewriteImprove : AssistantBaseCore public partial class AssistantRewriteImprove : AssistantBaseCore<SettingsDialogRewrite>
{ {
public override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT; public override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_SYNONYMS)] @attribute [Route(Routes.ASSISTANT_SYNONYMS)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogSynonyms>
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.Spellcheck" Adornment="Adornment.Start" Label="Your word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.Spellcheck" Adornment="Adornment.Start" Label="Your word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudTextField T="string" @bind-Text="@this.inputContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Lines="2" AutoGrow="@false" Label="(Optional) The context for the given word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudTextField T="string" @bind-Text="@this.inputContext" AdornmentIcon="@Icons.Material.Filled.Description" Adornment="Adornment.Start" Lines="2" AutoGrow="@false" Label="(Optional) The context for the given word or phrase" Variant="Variant.Outlined" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>

View File

@ -1,8 +1,9 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Synonym; namespace AIStudio.Assistants.Synonym;
public partial class AssistantSynonyms : AssistantBaseCore public partial class AssistantSynonyms : AssistantBaseCore<SettingsDialogSynonyms>
{ {
public override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT; public override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_SUMMARIZER)] @attribute [Route(Routes.ASSISTANT_SUMMARIZER)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogTextSummarizer>
@if (!this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader) @if (!this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader)
{ {

View File

@ -1,8 +1,9 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.TextSummarizer; namespace AIStudio.Assistants.TextSummarizer;
public partial class AssistantTextSummarizer : AssistantBaseCore public partial class AssistantTextSummarizer : AssistantBaseCore<SettingsDialogTextSummarizer>
{ {
public override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT; public override Tools.Components Component => Tools.Components.TEXT_SUMMARIZER_ASSISTANT;

View File

@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_TRANSLATION)] @attribute [Route(Routes.ASSISTANT_TRANSLATION)]
@inherits AssistantBaseCore @inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogTranslation>
@if (!this.SettingsManager.ConfigurationData.Translation.HideWebContentReader) @if (!this.SettingsManager.ConfigurationData.Translation.HideWebContentReader)
{ {

View File

@ -1,8 +1,9 @@
using AIStudio.Chat; using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Translation; namespace AIStudio.Assistants.Translation;
public partial class AssistantTranslation : AssistantBaseCore public partial class AssistantTranslation : AssistantBaseCore<SettingsDialogTranslation>
{ {
public override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT; public override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT;

View File

@ -1,4 +1,7 @@
using AIStudio.Components;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Chat; namespace AIStudio.Chat;
@ -27,6 +30,26 @@ public sealed record ChatThread
/// </summary> /// </summary>
public string SelectedProfile { get; set; } = string.Empty; public string SelectedProfile { get; set; } = string.Empty;
/// <summary>
/// The data source options for this chat thread.
/// </summary>
public DataSourceOptions DataSourceOptions { get; set; } = new();
/// <summary>
/// The AI-selected data sources for this chat thread.
/// </summary>
public IReadOnlyList<DataSourceAgentSelected> AISelectedDataSources { get; set; } = [];
/// <summary>
/// The augmented data for this chat thread. Will be inserted into the system prompt.
/// </summary>
public string AugmentedData { get; set; } = string.Empty;
/// <summary>
/// The data security to use, derived from the data sources used so far.
/// </summary>
public DataSourceSecurity DataSecurity { get; set; } = DataSourceSecurity.NOT_SPECIFIED;
/// <summary> /// <summary>
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user. /// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
/// </summary> /// </summary>
@ -61,31 +84,48 @@ public sealed record ChatThread
/// <returns>The prepared system prompt.</returns> /// <returns>The prepared system prompt.</returns>
public string PrepareSystemPrompt(SettingsManager settingsManager, ChatThread chatThread, ILogger logger) public string PrepareSystemPrompt(SettingsManager settingsManager, ChatThread chatThread, ILogger logger)
{ {
var isAugmentedDataAvailable = !string.IsNullOrWhiteSpace(chatThread.AugmentedData);
var systemPromptWithAugmentedData = isAugmentedDataAvailable switch
{
true => $"""
{chatThread.SystemPrompt}
{chatThread.AugmentedData}
""",
false => chatThread.SystemPrompt,
};
if(isAugmentedDataAvailable)
logger.LogInformation("Augmented data is available for the chat thread.");
else
logger.LogInformation("No augmented data is available for the chat thread.");
// //
// Prepare the system prompt: // Prepare the system prompt:
// //
string systemPromptText; string systemPromptText;
var logMessage = $"Using no profile for chat thread '{chatThread.Name}'."; var logMessage = $"Using no profile for chat thread '{chatThread.Name}'.";
if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile)) if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile))
systemPromptText = chatThread.SystemPrompt; systemPromptText = systemPromptWithAugmentedData;
else else
{ {
if(!Guid.TryParse(chatThread.SelectedProfile, out var profileId)) if(!Guid.TryParse(chatThread.SelectedProfile, out var profileId))
systemPromptText = chatThread.SystemPrompt; systemPromptText = systemPromptWithAugmentedData;
else else
{ {
if(chatThread.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty) if(chatThread.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
systemPromptText = chatThread.SystemPrompt; systemPromptText = systemPromptWithAugmentedData;
else else
{ {
var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatThread.SelectedProfile); var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatThread.SelectedProfile);
if(profile == default) if(profile == default)
systemPromptText = chatThread.SystemPrompt; systemPromptText = systemPromptWithAugmentedData;
else else
{ {
logMessage = $"Using profile '{profile.Name}' for chat thread '{chatThread.Name}'."; logMessage = $"Using profile '{profile.Name}' for chat thread '{chatThread.Name}'.";
systemPromptText = $""" systemPromptText = $"""
{chatThread.SystemPrompt} {systemPromptWithAugmentedData}
{profile.ToSystemPrompt()} {profile.ToSystemPrompt()}
"""; """;
@ -138,4 +178,46 @@ public sealed record ChatThread
// Remove the block from the chat thread: // Remove the block from the chat thread:
this.Blocks.Remove(block); this.Blocks.Remove(block);
} }
/// <summary>
/// Transforms this chat thread to an ERI chat thread.
/// </summary>
/// <param name="token">The cancellation token.</param>
/// <returns>The ERI chat thread.</returns>
public async Task<Tools.ERIClient.DataModel.ChatThread> ToERIChatThread(CancellationToken token = default)
{
//
// Transform the content blocks:
//
var contentBlocks = new List<Tools.ERIClient.DataModel.ContentBlock>(this.Blocks.Count);
foreach (var block in this.Blocks)
{
var (contentData, contentType) = block.Content switch
{
ContentImage image => (await image.AsBase64(token), Tools.ERIClient.DataModel.ContentType.IMAGE),
ContentText text => (text.Text, Tools.ERIClient.DataModel.ContentType.TEXT),
_ => (string.Empty, Tools.ERIClient.DataModel.ContentType.UNKNOWN),
};
contentBlocks.Add(new Tools.ERIClient.DataModel.ContentBlock
{
Role = block.Role switch
{
ChatRole.AI => Role.AI,
ChatRole.USER => Role.USER,
ChatRole.AGENT => Role.AGENT,
ChatRole.SYSTEM => Role.SYSTEM,
ChatRole.NONE => Role.NONE,
_ => Role.UNKNOWN,
},
Content = contentData,
Type = contentType,
});
}
return new Tools.ERIClient.DataModel.ChatThread { ContentBlocks = contentBlocks };
}
} }

View File

@ -0,0 +1,58 @@
using AIStudio.Provider.SelfHosted;
using AIStudio.Settings.DataModel;
namespace AIStudio.Chat;
public static class ChatThreadExtensions
{
/// <summary>
/// Checks if the specified provider is allowed for the chat thread.
/// </summary>
/// <remarks>
/// We don't check if the provider is allowed to use the data sources of the chat thread.
/// That kind of check is done in the RAG process itself.<br/><br/>
///
/// One thing which is not so obvious: after RAG was used on this thread, the entire chat
/// thread is kind of a data source by itself. Why? Because the augmentation data collected
/// from the data sources is stored in the chat thread. This means we must check if the
/// selected provider is allowed to use this thread's data.
/// </remarks>
/// <param name="chatThread">The chat thread to check.</param>
/// <param name="provider">The provider to check.</param>
/// <returns>True, when the provider is allowed for the chat thread. False, otherwise.</returns>
public static bool IsLLMProviderAllowed<T>(this ChatThread? chatThread, T provider)
{
// No chat thread available means we have a new chat. That's fine:
if (chatThread is null)
return true;
// The chat thread is available, but the data security is not specified.
// Means, we never used RAG or RAG was enabled, but no data sources were selected.
// That's fine as well:
if (chatThread.DataSecurity is DataSourceSecurity.NOT_SPECIFIED)
return true;
//
// Is the provider self-hosted?
//
var isSelfHostedProvider = provider switch
{
ProviderSelfHosted => true,
AIStudio.Settings.Provider p => p.IsSelfHosted,
_ => false,
};
//
// Check the chat data security against the selected provider:
//
return isSelfHostedProvider switch
{
// The provider is self-hosted -- we can use any data source:
true => true,
// The provider is not self-hosted -- it depends on the data security of the chat thread:
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
};
}
}

View File

@ -27,7 +27,7 @@
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null) @if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
{ {
<MudTooltip Text="Regenerate" Placement="Placement.Bottom"> <MudTooltip Text="Regenerate" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" OnClick="@this.RegenerateBlock"/> <MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
</MudTooltip> </MudTooltip>
} }
@if (this.RemoveBlockFunc is not null) @if (this.RemoveBlockFunc is not null)
@ -78,9 +78,9 @@
break; break;
case ContentType.IMAGE: case ContentType.IMAGE:
if (this.Content is ContentImage imageContent) if (this.Content is ContentImage { SourceType: ContentImageSource.URL or ContentImageSource.LOCAL_PATH } imageContent)
{ {
<MudImage Src="@imageContent.URL"/> <MudImage Src="@imageContent.Source"/>
} }
break; break;

View File

@ -1,9 +1,8 @@
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
namespace AIStudio.Chat; namespace AIStudio.Chat;
/// <summary> /// <summary>
@ -42,10 +41,10 @@ public partial class ContentBlockComponent : ComponentBase
public string Class { get; set; } = string.Empty; public string Class { get; set; } = string.Empty;
[Parameter] [Parameter]
public bool IsLastContentBlock { get; set; } = false; public bool IsLastContentBlock { get; set; }
[Parameter] [Parameter]
public bool IsSecondToLastBlock { get; set; } = false; public bool IsSecondToLastBlock { get; set; }
[Parameter] [Parameter]
public Func<IContent, Task>? RemoveBlockFunc { get; set; } public Func<IContent, Task>? RemoveBlockFunc { get; set; }

View File

@ -1,14 +1,13 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings;
namespace AIStudio.Chat; namespace AIStudio.Chat;
/// <summary> /// <summary>
/// Represents an image inside the chat. /// Represents an image inside the chat.
/// </summary> /// </summary>
public sealed class ContentImage : IContent public sealed class ContentImage : IContent, IImageSource
{ {
#region Implementation of IContent #region Implementation of IContent
@ -29,7 +28,7 @@ public sealed class ContentImage : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask; public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc /> /// <inheritdoc />
public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default) public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -37,12 +36,15 @@ public sealed class ContentImage : IContent
#endregion #endregion
/// <summary> /// <summary>
/// The URL of the image. /// The type of the image source.
/// </summary> /// </summary>
public string URL { get; set; } = string.Empty; /// <remarks>
/// Is the image source a URL, a local file path, a base64 string, etc.?
/// </remarks>
public required ContentImageSource SourceType { get; init; }
/// <summary> /// <summary>
/// The local path of the image. /// The image source.
/// </summary> /// </summary>
public string LocalPath { get; set; } = string.Empty; public required string Source { get; set; }
} }

View File

@ -0,0 +1,8 @@
namespace AIStudio.Chat;
public enum ContentImageSource
{
URL,
LOCAL_PATH,
BASE64,
}

View File

@ -2,6 +2,7 @@ using System.Text.Json.Serialization;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.RAG.RAGProcesses;
namespace AIStudio.Chat; namespace AIStudio.Chat;
@ -35,10 +36,32 @@ public sealed class ContentText : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask; public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <inheritdoc /> /// <inheritdoc />
public async Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread? chatThread, CancellationToken token = default) public async Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatThread, CancellationToken token = default)
{ {
if(chatThread is null) if(chatThread is null)
return; return new();
if(!chatThread.IsLLMProviderAllowed(provider))
{
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
logger.LogError("The provider is not allowed for this chat thread due to data security reasons. Skipping the AI process.");
return chatThread;
}
// Call the RAG process. Right now, we only have one RAG process:
if (lastPrompt is not null)
{
try
{
var rag = new AISrcSelWithRetCtxVal();
chatThread = await rag.ProcessAsync(provider, lastPrompt, chatThread, token);
}
catch (Exception e)
{
var logger = Program.SERVICE_PROVIDER.GetService<ILogger<ContentText>>()!;
logger.LogError(e, "Skipping the RAG process due to an error.");
}
}
// Store the last time we got a response. We use this later // Store the last time we got a response. We use this later
// to determine whether we should notify the UI about the // to determine whether we should notify the UI about the
@ -46,6 +69,9 @@ public sealed class ContentText : IContent
// the user chose. // the user chose.
var last = DateTimeOffset.Now; var last = DateTimeOffset.Now;
// Get the settings manager:
var settings = Program.SERVICE_PROVIDER.GetService<SettingsManager>()!;
// Start another thread by using a task to uncouple // Start another thread by using a task to uncouple
// the UI thread from the AI processing: // the UI thread from the AI processing:
await Task.Run(async () => await Task.Run(async () =>
@ -96,6 +122,7 @@ public sealed class ContentText : IContent
// Inform the UI that the streaming is done: // Inform the UI that the streaming is done:
await this.StreamingDone(); await this.StreamingDone();
return chatThread;
} }
#endregion #endregion

View File

@ -1,7 +1,6 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings;
namespace AIStudio.Chat; namespace AIStudio.Chat;
@ -42,5 +41,16 @@ public interface IContent
/// <summary> /// <summary>
/// Uses the provider to create the content. /// Uses the provider to create the content.
/// </summary> /// </summary>
public Task CreateFromProviderAsync(IProvider provider, SettingsManager settings, Model chatModel, ChatThread chatChatThread, CancellationToken token = default); public Task<ChatThread> CreateFromProviderAsync(IProvider provider, Model chatModel, IContent? lastPrompt, ChatThread? chatChatThread, CancellationToken token = default);
/// <summary>
/// Returns the corresponding ERI content type.
/// </summary>
public Tools.ERIClient.DataModel.ContentType ToERIContentType => this switch
{
ContentText => Tools.ERIClient.DataModel.ContentType.TEXT,
ContentImage => Tools.ERIClient.DataModel.ContentType.IMAGE,
_ => Tools.ERIClient.DataModel.ContentType.UNKNOWN,
};
} }

View File

@ -0,0 +1,17 @@
namespace AIStudio.Chat;
public interface IImageSource
{
/// <summary>
/// The type of the image source.
/// </summary>
/// <remarks>
/// Is the image source a URL, a local file path, a base64 string, etc.?
/// </remarks>
public ContentImageSource SourceType { get; init; }
/// <summary>
/// The image source.
/// </summary>
public string Source { get; set; }
}

View File

@ -0,0 +1,63 @@
namespace AIStudio.Chat;
public static class IImageSourceExtensions
{
/// <summary>
/// Read the image content as a base64 string.
/// </summary>
/// <remarks>
/// The images are directly converted to base64 strings. The maximum
/// size of the image is around 10 MB. If the image is larger, the method
/// returns an empty string.
///
/// As of now, this method does no sort of image processing. LLMs usually
/// do not work with arbitrary image sizes. In the future, we might have
/// to resize the images before sending them to the model.
/// </remarks>
/// <param name="image">The image source.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The image content as a base64 string; might be empty.</returns>
public static async Task<string> AsBase64(this IImageSource image, CancellationToken token = default)
{
switch (image.SourceType)
{
case ContentImageSource.BASE64:
return image.Source;
case ContentImageSource.URL:
{
using var httpClient = new HttpClient();
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, token);
if(response.IsSuccessStatusCode)
{
// Read the length of the content:
var lengthBytes = response.Content.Headers.ContentLength;
if(lengthBytes > 10_000_000)
return string.Empty;
var bytes = await response.Content.ReadAsByteArrayAsync(token);
return Convert.ToBase64String(bytes);
}
return string.Empty;
}
case ContentImageSource.LOCAL_PATH:
if(File.Exists(image.Source))
{
// Read the content length:
var length = new FileInfo(image.Source).Length;
if(length > 10_000_000)
return string.Empty;
var bytes = await File.ReadAllBytesAsync(image.Source, token);
return Convert.ToBase64String(bytes);
}
return string.Empty;
default:
return string.Empty;
}
}
}

View File

@ -1,12 +0,0 @@
namespace AIStudio.Chat;
/// <summary>
/// Data about a workspace.
/// </summary>
/// <param name="name">The name of the workspace.</param>
public sealed class Workspace(string name)
{
public string Name { get; set; } = name;
public List<ChatThread> Threads { get; set; } = new();
}

View File

@ -1,3 +1,5 @@
@typeparam TSettings
<MudCard Outlined="@true" Style="@this.BlockStyle"> <MudCard Outlined="@true" Style="@this.BlockStyle">
<MudCardHeader> <MudCardHeader>
<CardHeaderContent> <CardHeaderContent>
@ -17,8 +19,11 @@
</MudStack> </MudStack>
</MudCardContent> </MudCardContent>
<MudCardActions> <MudCardActions>
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link"> <MudButtonGroup Variant="Variant.Outlined">
@this.ButtonText <MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link">
</MudButton> @this.ButtonText
</MudButton>
<MudIconButton Variant="Variant.Text" Icon="@Icons.Material.Filled.Settings" Color="Color.Default" OnClick="@this.OpenSettingsDialog"/>
</MudButtonGroup>
</MudCardActions> </MudCardActions>
</MudCard> </MudCard>

View File

@ -2,9 +2,11 @@ using AIStudio.Settings;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDisposable public partial class AssistantBlock<TSettings> : ComponentBase, IMessageBusReceiver, IDisposable where TSettings : IComponent
{ {
[Parameter] [Parameter]
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@ -30,6 +32,16 @@ public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDispo
[Inject] [Inject]
private MessageBus MessageBus { get; init; } = null!; private MessageBus MessageBus { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
private async Task OpenSettingsDialog()
{
var dialogParameters = new DialogParameters();
await this.DialogService.ShowAsync<TSettings>("Open Settings", dialogParameters, DialogOptions.FULLSCREEN);
}
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
@ -44,6 +56,8 @@ public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDispo
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(AssistantBlock<TSettings>);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)

View File

@ -13,6 +13,19 @@ public partial class Changelog
public static readonly Log[] LOGS = public static readonly Log[] LOGS =
[ [
new (213, "v0.9.38, build 213 (2025-03-17 18:18 UTC)", "v0.9.38.md"),
new (212, "v0.9.37, build 212 (2025-03-16 20:32 UTC)", "v0.9.37.md"),
new (211, "v0.9.36, build 211 (2025-03-15 10:42 UTC)", "v0.9.36.md"),
new (210, "v0.9.35, build 210 (2025-03-13 08:44 UTC)", "v0.9.35.md"),
new (209, "v0.9.34, build 209 (2025-03-11 13:02 UTC)", "v0.9.34.md"),
new (208, "v0.9.33, build 208 (2025-03-11 08:14 UTC)", "v0.9.33.md"),
new (207, "v0.9.32, build 207 (2025-03-08 20:15 UTC)", "v0.9.32.md"),
new (206, "v0.9.31, build 206 (2025-03-03 15:33 UTC)", "v0.9.31.md"),
new (205, "v0.9.30, build 205 (2025-02-24 19:55 UTC)", "v0.9.30.md"),
new (204, "v0.9.29, build 204 (2025-02-24 13:48 UTC)", "v0.9.29.md"),
new (203, "v0.9.28, build 203 (2025-02-09 16:33 UTC)", "v0.9.28.md"),
new (202, "v0.9.27, build 202 (2025-01-21 18:24 UTC)", "v0.9.27.md"),
new (201, "v0.9.26, build 201 (2025-01-13 19:11 UTC)", "v0.9.26.md"),
new (200, "v0.9.25, build 200 (2025-01-04 18:33 UTC)", "v0.9.25.md"), new (200, "v0.9.25, build 200 (2025-01-04 18:33 UTC)", "v0.9.25.md"),
new (199, "v0.9.24, build 199 (2025-01-04 11:40 UTC)", "v0.9.24.md"), new (199, "v0.9.24, build 199 (2025-01-04 11:40 UTC)", "v0.9.24.md"),
new (198, "v0.9.23, build 198 (2025-01-02 19:39 UTC)", "v0.9.23.md"), new (198, "v0.9.23, build 198 (2025-01-02 19:39 UTC)", "v0.9.23.md"),

View File

@ -23,7 +23,7 @@ public partial class Changelog : ComponentBase
private async Task ReadLogAsync() private async Task ReadLogAsync()
{ {
var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}"); using var response = await this.HttpClient.GetAsync($"changelog/{this.SelectedLog.Filename}");
this.LogContent = await response.Content.ReadAsStringAsync(); this.LogContent = await response.Content.ReadAsStringAsync();
} }
} }

View File

@ -3,7 +3,7 @@
@inherits MSGComponentBase @inherits MSGComponentBase
<InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" HeaderHeight="12.3em" MinWidth="36em"> <InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" MinWidth="36em" Style="height: 100%">
<ChildContent> <ChildContent>
@if (this.ChatThread is not null) @if (this.ChatThread is not null)
{ {
@ -24,7 +24,7 @@
IsLastContentBlock="@isLastBlock" IsLastContentBlock="@isLastBlock"
IsSecondToLastBlock="@isSecondLastBlock" IsSecondToLastBlock="@isSecondLastBlock"
RegenerateFunc="@this.RegenerateBlock" RegenerateFunc="@this.RegenerateBlock"
RegenerateEnabled="@(() => this.IsProviderSelected)" RegenerateEnabled="@(() => this.IsProviderSelected && this.ChatThread.IsLLMProviderAllowed(this.Provider))"
EditLastBlockFunc="@this.EditLastBlock" EditLastBlockFunc="@this.EditLastBlock"
EditLastUserBlockFunc="@this.EditLastUserBlock"/> EditLastUserBlockFunc="@this.EditLastUserBlock"/>
} }
@ -46,7 +46,7 @@
Adornment="Adornment.End" Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Send" AdornmentIcon="@Icons.Material.Filled.Send"
OnAdornmentClick="() => this.SendMessage()" OnAdornmentClick="() => this.SendMessage()"
ReadOnly="!this.IsProviderSelected || this.isStreaming" Disabled="@this.IsInputForbidden()"
Immediate="@true" Immediate="@true"
OnKeyUp="this.InputKeyEvent" OnKeyUp="this.InputKeyEvent"
UserAttributes="@USER_INPUT_ATTRIBUTES" UserAttributes="@USER_INPUT_ATTRIBUTES"
@ -97,7 +97,7 @@
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence) @if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{ {
<ConfidenceInfo Mode="ConfidenceInfoMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/> <ConfidenceInfo Mode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
} }
@if (this.isStreaming && this.cancellationTokenSource is not null) @if (this.isStreaming && this.cancellationTokenSource is not null)
@ -108,6 +108,19 @@
} }
<ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged"/> <ProfileSelection CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged"/>
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
{
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.BUTTON" PopoverButtonClasses="ma-3" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/>
}
@if (!this.ChatThread.IsLLMProviderAllowed(this.Provider))
{
<MudTooltip Text="The selected provider is not allowed in this chat due to data security reasons." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Error"/>
</MudTooltip>
}
<MudIconButton />
</MudToolBar> </MudToolBar>
</FooterContent> </FooterContent>
</InnerScrolling> </InnerScrolling>

View File

@ -40,9 +40,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Bottom; private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private DataSourceSelection? dataSourceSelectionComponent;
private DataSourceOptions earlyDataSourceOptions = new();
private Profile currentProfile = Profile.NO_PROFILE; private Profile currentProfile = Profile.NO_PROFILE;
private bool hasUnsavedChanges; private bool hasUnsavedChanges;
private bool mustScrollToBottomAfterRender; private bool mustScrollToBottomAfterRender;
@ -66,21 +68,40 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
protected override async Task OnInitializedAsync() 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.WORKSPACE_LOADED_CHAT_CHANGED ]);
// Configure the spellchecking for the user input: // Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
// Get the preselected profile:
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT); this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
//
// Check for deferred messages of the kind 'SEND_TO_CHAT',
// aka the user sends an assistant result to the chat:
//
var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault(); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages<ChatThread>(Event.SEND_TO_CHAT).FirstOrDefault();
if (deferredContent is not null) if (deferredContent is not null)
{ {
//
// Yes, the user sent an assistant result to the chat.
//
// Use chat thread sent by the user:
this.ChatThread = deferredContent; this.ChatThread = deferredContent;
this.Logger.LogInformation($"The chat '{this.ChatThread.Name}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now."); this.Logger.LogInformation($"The chat '{this.ChatThread.Name}' with {this.ChatThread.Blocks.Count} messages was deferred and will be rendered now.");
await this.ChatThreadChanged.InvokeAsync(this.ChatThread); await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
// We know already that the chat thread is not null,
// but we have to check it again for the nullability
// for the compiler:
if (this.ChatThread is not null) if (this.ChatThread is not null)
{ {
//
// Check if the chat thread has a name. If not, we
// generate the name now:
//
if (string.IsNullOrWhiteSpace(this.ChatThread.Name)) if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
{ {
var firstUserBlock = this.ChatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER); var firstUserBlock = this.ChatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER);
@ -94,12 +115,24 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
} }
} }
//
// Check if the user wants to apply the standard chat data source options:
//
if (this.SettingsManager.ConfigurationData.Chat.SendToChatDataSourceBehavior is SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS)
this.ChatThread.DataSourceOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
//
// Check if the user wants to store the chat automatically:
//
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{ {
this.autoSaveEnabled = true; this.autoSaveEnabled = true;
this.mustStoreChat = true; this.mustStoreChat = true;
// Ensure the workspace exists: //
// When a standard workspace is used, we have to ensure
// that the workspace is available:
//
if(this.ChatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID) if(this.ChatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID)
await WorkspaceBehaviour.EnsureERIServerWorkspace(); await WorkspaceBehaviour.EnsureERIServerWorkspace();
@ -108,14 +141,38 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
} }
} }
} }
else
{
//
// No, the user did not send an assistant result to the chat.
//
this.ApplyStandardDataSourceOptions();
}
//
// Check if the user wants to show the latest message after loading:
//
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{ {
//
// We cannot scroll to the bottom right now because the
// chat component is not rendered yet. We have to wait for
// the rendering process to finish. Thus, we set a flag
// to scroll to the bottom after the rendering process.:
//
this.mustScrollToBottomAfterRender = true; this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 4; this.scrollRenderCountdown = 4;
this.StateHasChanged(); this.StateHasChanged();
} }
//
// Check if another component deferred the loading of a chat.
//
// This is used, e.g., for the bias-of-the-day component:
// when the bias for this day was already produced, the bias
// component sends a message to the chat component to load
// the chat with the bias:
//
var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages<LoadChat>(Event.LOAD_CHAT).FirstOrDefault(); var deferredLoading = MessageBus.INSTANCE.CheckDeferredMessages<LoadChat>(Event.LOAD_CHAT).FirstOrDefault();
if (deferredLoading != default) if (deferredLoading != default)
{ {
@ -124,6 +181,19 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.Logger.LogInformation($"The loading of the chat '{this.loadChat.ChatId}' was deferred and will be loaded now."); this.Logger.LogInformation($"The loading of the chat '{this.loadChat.ChatId}' was deferred and will be loaded now.");
} }
//
// When for whatever reason we have a chat thread, we have to
// ensure that the corresponding workspace id is set and the
// workspace name is loaded:
//
if (this.ChatThread is not null)
{
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
}
// Select the correct provider:
await this.SelectProviderWhenLoadingChat(); await this.SelectProviderWhenLoadingChat();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }
@ -197,6 +267,16 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty; private string UserInputClass => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? "confidence-border" : string.Empty;
private void ApplyStandardDataSourceOptions()
{
var chatDefaultOptions = this.SettingsManager.ConfigurationData.Chat.PreselectedDataSourceOptions.CreateCopy();
this.earlyDataSourceOptions = chatDefaultOptions;
if(this.ChatThread is not null)
this.ChatThread.DataSourceOptions = chatDefaultOptions;
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(chatDefaultOptions);
}
private string ExtractThreadName(string firstUserInput) private string ExtractThreadName(string firstUserInput)
{ {
// We select the first 10 words of the user input: // We select the first 10 words of the user input:
@ -224,8 +304,57 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
await this.ChatThreadChanged.InvokeAsync(this.ChatThread); await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
} }
private IReadOnlyList<DataSourceAgentSelected> GetAgentSelectedDataSources()
{
if (this.ChatThread is null)
return [];
return this.ChatThread.AISelectedDataSources;
}
private DataSourceOptions GetCurrentDataSourceOptions()
{
if (this.ChatThread is not null)
return this.ChatThread.DataSourceOptions;
return this.earlyDataSourceOptions;
}
private async Task SetCurrentDataSourceOptions(DataSourceOptions updatedOptions)
{
if (this.ChatThread is not null)
{
this.hasUnsavedChanges = true;
this.ChatThread.DataSourceOptions = updatedOptions;
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
await this.SaveThread();
this.hasUnsavedChanges = false;
}
}
else
this.earlyDataSourceOptions = updatedOptions;
}
private bool IsInputForbidden()
{
if (!this.IsProviderSelected)
return true;
if(this.isStreaming)
return true;
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
return true;
return false;
}
private async Task InputKeyEvent(KeyboardEventArgs keyEvent) private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
{ {
if(this.dataSourceSelectionComponent?.IsVisible ?? false)
this.dataSourceSelectionComponent.Hide();
this.hasUnsavedChanges = true; this.hasUnsavedChanges = true;
var key = keyEvent.Code.ToLowerInvariant(); var key = keyEvent.Code.ToLowerInvariant();
@ -255,6 +384,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if (!this.IsProviderSelected) if (!this.IsProviderSelected)
return; return;
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
return;
// We need to blur the focus away from the input field // We need to blur the focus away from the input field
// to be able to clear the field: // to be able to clear the field:
await this.inputField.BlurAsync(); await this.inputField.BlurAsync();
@ -269,6 +401,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
SystemPrompt = SystemPrompts.DEFAULT, SystemPrompt = SystemPrompts.DEFAULT,
WorkspaceId = this.currentWorkspaceId, WorkspaceId = this.currentWorkspaceId,
ChatId = Guid.NewGuid(), ChatId = Guid.NewGuid(),
DataSourceOptions = this.earlyDataSourceOptions,
Name = this.ExtractThreadName(this.userInput), Name = this.ExtractThreadName(this.userInput),
Seed = this.RNG.Next(), Seed = this.RNG.Next(),
Blocks = [], Blocks = [],
@ -288,8 +421,14 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
} }
var time = DateTimeOffset.Now; var time = DateTimeOffset.Now;
IContent? lastUserPrompt;
if (!reuseLastUserPrompt) if (!reuseLastUserPrompt)
{ {
lastUserPrompt = new ContentText
{
Text = this.userInput,
};
// //
// Add the user message to the thread: // Add the user message to the thread:
// //
@ -298,10 +437,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
Time = time, Time = time,
ContentType = ContentType.TEXT, ContentType = ContentType.TEXT,
Role = ChatRole.USER, Role = ChatRole.USER,
Content = new ContentText Content = lastUserPrompt,
{
Text = this.userInput,
},
}); });
// Save the chat: // Save the chat:
@ -312,6 +448,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.StateHasChanged(); this.StateHasChanged();
} }
} }
else
lastUserPrompt = this.ChatThread.Blocks.Last(x => x.Role is ChatRole.USER).Content;
// //
// Add the AI response to the thread: // Add the AI response to the thread:
@ -353,7 +491,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Use the selected provider to get the AI response. // Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire // By awaiting this line, we wait for the entire
// content to be streamed. // content to be streamed.
await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.SettingsManager, this.Provider.Model, this.ChatThread, this.cancellationTokenSource.Token); this.ChatThread = await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token);
} }
this.cancellationTokenSource = null; this.cancellationTokenSource = null;
@ -367,6 +505,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Disable the stream state: // Disable the stream state:
this.isStreaming = false; this.isStreaming = false;
// Update the UI:
this.StateHasChanged(); this.StateHasChanged();
} }
@ -400,6 +540,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false) private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false)
{ {
//
// 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)
{ {
var dialogParameters = new DialogParameters var dialogParameters = new DialogParameters
@ -413,6 +557,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
return; return;
} }
//
// Delete the previous chat when desired and necessary:
//
if (this.ChatThread is not null && deletePreviousChat) if (this.ChatThread is not null && deletePreviousChat)
{ {
string chatPath; string chatPath;
@ -427,10 +574,16 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true); await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true);
} }
//
// Reset our state:
//
this.isStreaming = false; this.isStreaming = false;
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
this.userInput = string.Empty; this.userInput = string.Empty;
//
// Reset the LLM provider considering the user's settings:
//
switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior) switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior)
{ {
case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER: case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER:
@ -449,8 +602,16 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
break; break;
} }
//
// Reset the chat thread or create a new one:
//
if (!useSameWorkspace) if (!useSameWorkspace)
{ {
//
// When the user wants to start a new chat outside the current workspace,
// we have to reset the workspace id and the workspace name. Also, we have
// to reset the chat thread:
//
this.ChatThread = null; this.ChatThread = null;
this.currentWorkspaceId = Guid.Empty; this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty; this.currentWorkspaceName = string.Empty;
@ -458,6 +619,11 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
} }
else else
{ {
//
// When the user wants to start a new chat in the same workspace, we have to
// reset the chat thread only. The workspace id and the workspace name remain
// the same:
//
this.ChatThread = new() this.ChatThread = new()
{ {
SelectedProvider = this.Provider.Id, SelectedProvider = this.Provider.Id,
@ -471,7 +637,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
}; };
} }
this.userInput = string.Empty; // Now, we have to reset the data source options as well:
this.ApplyStandardDataSourceOptions();
// Notify the parent component about the change:
await this.ChatThreadChanged.InvokeAsync(this.ChatThread); await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
} }
@ -525,17 +694,30 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.isStreaming = false; this.isStreaming = false;
this.hasUnsavedChanges = false; this.hasUnsavedChanges = false;
this.userInput = string.Empty; this.userInput = string.Empty;
this.currentWorkspaceId = this.ChatThread?.WorkspaceId ?? Guid.Empty;
this.currentWorkspaceName = this.ChatThread is null ? string.Empty : await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId); if (this.ChatThread is not null)
this.WorkspaceName(this.currentWorkspaceName); {
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
this.dataSourceSelectionComponent?.ChangeOptionWithoutSaving(this.ChatThread.DataSourceOptions, this.ChatThread.AISelectedDataSources);
}
else
{
this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
this.ApplyStandardDataSourceOptions();
}
await this.SelectProviderWhenLoadingChat(); await this.SelectProviderWhenLoadingChat();
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading) if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{ {
this.mustScrollToBottomAfterRender = true; this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2; this.scrollRenderCountdown = 2;
this.StateHasChanged();
} }
this.StateHasChanged();
} }
private async Task ResetState() private async Task ResetState()
@ -549,6 +731,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.WorkspaceName(this.currentWorkspaceName); this.WorkspaceName(this.currentWorkspaceName);
this.ChatThread = null; this.ChatThread = null;
this.ApplyStandardDataSourceOptions();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread); await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
} }
@ -606,6 +789,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
if(this.ChatThread is null) if(this.ChatThread is null)
return; return;
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
return;
this.ChatThread.Remove(aiBlock, removeForRegenerate: true); this.ChatThread.Remove(aiBlock, removeForRegenerate: true);
this.hasUnsavedChanges = true; this.hasUnsavedChanges = true;
this.StateHasChanged(); this.StateHasChanged();
@ -653,6 +839,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
public override string ComponentName => nameof(ChatComponent);
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{ {
switch (triggeredEvent) switch (triggeredEvent)
@ -692,6 +880,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
this.MessageBus.Unregister(this);
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{ {
await this.SaveThread(); await this.SaveThread();

View File

@ -1,7 +1,7 @@
@using AIStudio.Provider @using AIStudio.Provider
<div class="d-flex"> <div class="d-flex">
<MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider."> <MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider." Placement="Placement.Top">
@if (this.Mode is ConfidenceInfoMode.ICON) @if (this.Mode is PopoverTriggerMode.ICON)
{ {
<MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/> <MudIconButton Icon="@Icons.Material.Filled.Security" Class="confidence-icon" Style="@this.LLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager)" OnClick="@(() => this.ToggleConfidence())"/>
} }
@ -20,7 +20,7 @@
<MudText Typo="Typo.h5">Confidence Card</MudText> <MudText Typo="Typo.h5">Confidence Card</MudText>
</CardHeaderContent> </CardHeaderContent>
</MudCardHeader> </MudCardHeader>
<MudCardContent> <MudCardContent Style="max-height: 50vh; max-width: 35vw; overflow: auto;">
<MudText Typo="Typo.h6">Description</MudText> <MudText Typo="Typo.h6">Description</MudText>
<MudMarkdown Value="@this.currentConfidence.Description"/> <MudMarkdown Value="@this.currentConfidence.Description"/>

View File

@ -8,7 +8,7 @@ namespace AIStudio.Components;
public partial class ConfidenceInfo : ComponentBase, IMessageBusReceiver, IDisposable public partial class ConfidenceInfo : ComponentBase, IMessageBusReceiver, IDisposable
{ {
[Parameter] [Parameter]
public ConfidenceInfoMode Mode { get; set; } = ConfidenceInfoMode.BUTTON; public PopoverTriggerMode Mode { get; set; } = PopoverTriggerMode.BUTTON;
[Parameter] [Parameter]
public LLMProviders LLMProvider { get; set; } public LLMProviders LLMProvider { get; set; }
@ -59,10 +59,12 @@ public partial class ConfidenceInfo : ComponentBase, IMessageBusReceiver, IDispo
private string GetCurrentConfidenceColor() => $"color: {this.currentConfidence.Level.GetColor(this.SettingsManager)};"; private string GetCurrentConfidenceColor() => $"color: {this.currentConfidence.Level.GetColor(this.SettingsManager)};";
private string GetPopoverStyle() => $"border-color: {this.currentConfidence.Level.GetColor(this.SettingsManager)}; max-width: calc(35vw);"; private string GetPopoverStyle() => $"border-color: {this.currentConfidence.Level.GetColor(this.SettingsManager)};";
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfidenceInfo);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)

View File

@ -57,6 +57,8 @@ public partial class ConfigurationBase : ComponentBase, IMessageBusReceiver, IDi
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfigurationBase);
public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data) public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)

View File

@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
@ -47,8 +49,12 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
#endregion #endregion
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private IEnumerable<ConfigurationSelectData<string>> FilteredData() private IEnumerable<ConfigurationSelectData<string>> FilteredData()
{ {
if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS)
yield return new("Use app default", string.Empty);
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component); var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.Component);
foreach (var providerId in this.Data) foreach (var providerId in this.Data)
{ {
@ -60,6 +66,8 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfigurationProviderSelection);
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)

View File

@ -0,0 +1,25 @@
using AIStudio.Agents;
using AIStudio.Settings;
namespace AIStudio.Components;
/// <summary>
/// A data structure to combine the data source and the underlying AI decision.
/// </summary>
public sealed class DataSourceAgentSelected
{
/// <summary>
/// The data source.
/// </summary>
public required IDataSource DataSource { get; set; }
/// <summary>
/// The AI decision, which led to the selection of the data source.
/// </summary>
public required SelectedDataSource AIDecision { get; set; }
/// <summary>
/// Indicates whether the data source is part of the final selection for the RAG process.
/// </summary>
public bool Selected { get; set; }
}

View File

@ -0,0 +1,181 @@
@using AIStudio.Settings
@if (this.SelectionMode is DataSourceSelectionMode.SELECTION_MODE)
{
<div class="d-flex">
<MudTooltip Text="Select the data you want to use here." Placement="Placement.Top">
@if (this.PopoverTriggerMode is PopoverTriggerMode.ICON)
{
<MudIconButton Icon="@Icons.Material.Filled.Source" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())"/>
}
else
{
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.Source" Class="@this.PopoverButtonClasses" OnClick="@(() => this.ToggleDataSourceSelection())">
Select data
</MudButton>
}
</MudTooltip>
<MudPopover Open="@this.showDataSourceSelection" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" DropShadow="@true" Class="border-solid border-4 rounded-lg">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<PreviewPrototype/>
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudText Typo="Typo.h5">Data Source Selection</MudText>
<MudSpacer/>
<MudTooltip Text="Manage your data sources" Placement="Placement.Top">
<MudIconButton Variant="Variant.Filled" Icon="@Icons.Material.Filled.Settings" OnClick="@this.OpenSettingsDialog"/>
</MudTooltip>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Style="min-width: 24em; max-height: 60vh; max-width: 45vw; overflow: auto;">
@if (this.waitingForDataSources)
{
<MudSkeleton Width="30%" Height="42px;"/>
<MudSkeleton Width="80%"/>
<MudSkeleton Width="100%"/>
}
else if (this.SettingsManager.ConfigurationData.DataSources.Count == 0)
{
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
You haven't configured any data sources. To grant the AI access to your data, you need to
add such a source. However, if you wish to use data from your device, you first have to set up
a so-called embedding. This embedding is necessary so the AI can effectively search your data,
find and retrieve the correct information required for each task. In addition to local data,
you can also incorporate your company's data. To do so, your company must provide the data through
an ERI (External Retrieval Interface).
</MudJustifiedText>
<MudStack StretchItems="StretchItems.None" AlignItems="AlignItems.Start">
<MudButton Variant="Variant.Filled" OnClick="this.OpenSettingsDialog" StartIcon="@Icons.Material.Filled.Settings">
Manage Data Sources
</MudButton>
<MudButton Variant="Variant.Filled" Href="https://mindworkai.org/#eri---external-retrieval-interface" Target="_blank" StartIcon="@Icons.Material.Filled.Settings">
Read more about ERI
</MudButton>
</MudStack>
}
else if (this.showDataSourceSelection)
{
<MudTextSwitch Label="Are data sources enabled?" Value="@this.areDataSourcesEnabled" LabelOn="Yes, I want to use data sources." LabelOff="No, I don't want to use data sources." ValueChanged="@this.EnabledChanged"/>
@if (this.areDataSourcesEnabled)
{
<MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation)
{
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
}
@switch (this.aiBasedSourceSelection)
{
case true when this.availableDataSources.Count == 0:
<MudText Typo="Typo.body1" Class="mb-3">
Your data sources cannot be used with the LLM provider you selected due to data privacy, or they are currently unavailable.
</MudText>
break;
case true when this.DataSourcesAISelected.Count == 0:
<MudText Typo="Typo.body1" Class="mb-3">
The AI evaluates each of your inputs to determine whether and which data sources are necessary. Currently, the AI has not selected any source.
</MudText>
break;
case false when this.availableDataSources.Count == 0:
<MudText Typo="Typo.body1" Class="mb-3">
Your data sources cannot be used with the LLM provider you selected due to data privacy, or they are currently unavailable.
</MudText>
break;
case false:
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))" Style="max-height: 14em;">
@foreach (var source in this.availableDataSources)
{
<MudListItem Value="@source">
@source.Name
</MudListItem>
}
</MudList>
</MudField>
break;
case true:
<MudExpansionPanels MultiExpansion="@false" Class="mt-3" Style="max-height: 14em;">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.TouchApp" HeaderText="Available Data Sources">
<MudList T="IDataSource" SelectionMode="MudBlazor.SelectionMode.SingleSelection" SelectedValues="@this.selectedDataSources" Style="max-height: 14em;">
@foreach (var source in this.availableDataSources)
{
<MudListItem Value="@source">
@source.Name
</MudListItem>
}
</MudList>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Filter" HeaderText="AI-Selected Data Sources">
<MudList T="DataSourceAgentSelected" SelectionMode="MudBlazor.SelectionMode.MultiSelection" ReadOnly="@true" SelectedValues="@this.GetSelectedDataSourcesWithAI()" Style="max-height: 14em;">
@foreach (var source in this.DataSourcesAISelected)
{
<MudListItem Value="@source">
<ChildContent>
<MudText Typo="Typo.body1">
@source.DataSource.Name
</MudText>
<MudProgressLinear Color="Color.Info" Min="0" Max="1" Value="@source.AIDecision.Confidence"/>
<MudJustifiedText Typo="Typo.body2">
@(this.GetAIReasoning(source))
</MudJustifiedText>
</ChildContent>
</MudListItem>
}
</MudList>
</ExpansionPanel>
</MudExpansionPanels>
break;
}
}
}
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" OnClick="@(() => this.HideDataSourceSelection())">
Close
</MudButton>
</MudCardActions>
</MudCard>
</MudPopover>
</div>
}
else if (this.SelectionMode is DataSourceSelectionMode.CONFIGURATION_MODE)
{
<MudPaper Class="pa-3 mb-8 mt-3 border-dashed border rounded-lg">
<PreviewPrototype/>
<MudText Typo="Typo.h5">Data Source Selection</MudText>
@if (!string.IsNullOrWhiteSpace(this.ConfigurationHeaderMessage))
{
<MudText Typo="Typo.body1">
@this.ConfigurationHeaderMessage
</MudText>
}
<MudTextSwitch Label="Are data sources enabled?" Value="@this.areDataSourcesEnabled" LabelOn="Yes, I want to use data sources." LabelOff="No, I don't want to use data sources." ValueChanged="@this.EnabledChanged"/>
@if (this.areDataSourcesEnabled)
{
<MudTextSwitch Label="AI-based data source selection" Value="@this.aiBasedSourceSelection" LabelOn="Yes, let the AI decide which data sources are needed." LabelOff="No, I manually decide which data source to use." ValueChanged="@this.AutoModeChanged"/>
<MudTextSwitch Label="AI-based data validation" Value="@this.aiBasedValidation" LabelOn="Yes, let the AI validate & filter the retrieved data." LabelOff="No, use all data retrieved from the data sources." ValueChanged="@this.ValidationModeChanged"/>
<MudField Label="Available Data Sources" Variant="Variant.Outlined" Class="mb-3" Disabled="@this.aiBasedSourceSelection">
<MudList T="IDataSource" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@(x => this.SelectionChanged(x))">
@foreach (var source in this.availableDataSources)
{
<MudListItem Value="@source">
@source.Name
</MudListItem>
}
</MudList>
</MudField>
}
</MudPaper>
}

View File

@ -0,0 +1,295 @@
using AIStudio.Dialogs.Settings;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components;
public partial class DataSourceSelection : ComponentBase, IMessageBusReceiver, IDisposable
{
[Parameter]
public DataSourceSelectionMode SelectionMode { get; set; } = DataSourceSelectionMode.SELECTION_MODE;
[Parameter]
public PopoverTriggerMode PopoverTriggerMode { get; set; } = PopoverTriggerMode.BUTTON;
[Parameter]
public string PopoverButtonClasses { get; set; } = string.Empty;
[Parameter]
public required AIStudio.Settings.Provider LLMProvider { get; set; }
[Parameter]
public required DataSourceOptions DataSourceOptions { get; set; }
[Parameter]
public EventCallback<DataSourceOptions> DataSourceOptionsChanged { get; set; }
[Parameter]
public IReadOnlyList<DataSourceAgentSelected> DataSourcesAISelected { get; set; } = [];
[Parameter]
public string ConfigurationHeaderMessage { get; set; } = string.Empty;
[Parameter]
public bool AutoSaveAppSettings { get; set; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private MessageBus MessageBus { get; init; } = null!;
[Inject]
private DataSourceService DataSourceService { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
private bool internalChange;
private bool showDataSourceSelection;
private bool waitingForDataSources = true;
private IReadOnlyList<IDataSource> availableDataSources = [];
private IReadOnlyCollection<IDataSource> selectedDataSources = [];
private bool aiBasedSourceSelection;
private bool aiBasedValidation;
private bool areDataSourcesEnabled;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.COLOR_THEME_CHANGED, Event.RAG_AUTO_DATA_SOURCES_SELECTED ]);
//
// Load the settings:
//
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
this.waitingForDataSources = this.areDataSourcesEnabled;
//
// Preselect the data sources. Right now, we cannot filter
// the data sources. Later, when the component is shown, we
// will filter the data sources.
//
// Right before the preselection would be used to kick off the
// RAG process, we will filter the data sources as well.
//
var preselectedSources = new List<IDataSource>(this.DataSourceOptions.PreselectedDataSourceIds.Count);
foreach (var preselectedDataSourceId in this.DataSourceOptions.PreselectedDataSourceIds)
{
var dataSource = this.SettingsManager.ConfigurationData.DataSources.FirstOrDefault(ds => ds.Id == preselectedDataSourceId);
if (dataSource is not null)
preselectedSources.Add(dataSource);
}
this.selectedDataSources = preselectedSources;
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
if (!this.internalChange)
{
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
}
switch (this.SelectionMode)
{
//
// In selection mode, we have to load & filter the data sources
// when the component is shown:
//
case DataSourceSelectionMode.SELECTION_MODE:
//
// For external changes, we have to reload & filter
// the data sources:
//
if (this.showDataSourceSelection && !this.internalChange)
await this.LoadAndApplyFilters();
else
this.internalChange = false;
break;
//
// In configuration mode, we have to load all data sources:
//
case DataSourceSelectionMode.CONFIGURATION_MODE:
this.availableDataSources = this.SettingsManager.ConfigurationData.DataSources;
break;
}
await base.OnParametersSetAsync();
}
#endregion
private async Task OpenSettingsDialog()
{
this.showDataSourceSelection = false;
this.StateHasChanged();
var dialogParameters = new DialogParameters();
var dialogReference = await this.DialogService.ShowAsync<SettingsDialogDataSources>(null, dialogParameters, DialogOptions.FULLSCREEN);
await dialogReference.Result;
await this.LoadAndApplyFilters();
this.showDataSourceSelection = true;
this.StateHasChanged();
}
private SelectionMode GetListSelectionMode() => this.aiBasedSourceSelection ? MudBlazor.SelectionMode.SingleSelection : MudBlazor.SelectionMode.MultiSelection;
private IReadOnlyCollection<DataSourceAgentSelected> GetSelectedDataSourcesWithAI() => this.DataSourcesAISelected.Where(n => n.Selected).ToList();
private string GetAIReasoning(DataSourceAgentSelected source) => $"AI reasoning (confidence {source.AIDecision.Confidence:P0}): {source.AIDecision.Reason}";
public void ChangeOptionWithoutSaving(DataSourceOptions options, IReadOnlyList<DataSourceAgentSelected>? aiSelectedDataSources = null)
{
this.DataSourceOptions = options;
this.DataSourcesAISelected = aiSelectedDataSources ?? [];
this.aiBasedSourceSelection = this.DataSourceOptions.AutomaticDataSourceSelection;
this.aiBasedValidation = this.DataSourceOptions.AutomaticValidation;
this.areDataSourcesEnabled = !this.DataSourceOptions.DisableDataSources;
this.selectedDataSources = this.SettingsManager.ConfigurationData.DataSources.Where(ds => this.DataSourceOptions.PreselectedDataSourceIds.Contains(ds.Id)).ToList();
this.waitingForDataSources = false;
//
// Remark: We do not apply the filters here. This is done later
// when either the parameters are changed or just before the
// RAG process is started (outside of this component).
//
// In fact, when we apply the filters here, multiple calls
// to the filter method would be made. We would get conflicts.
//
}
public bool IsVisible => this.showDataSourceSelection;
public void Hide()
{
this.showDataSourceSelection = false;
this.StateHasChanged();
}
private async Task LoadAndApplyFilters()
{
if(this.DataSourceOptions.DisableDataSources)
return;
this.waitingForDataSources = true;
this.StateHasChanged();
// Load the data sources:
var sources = await this.DataSourceService.GetDataSources(this.LLMProvider, this.selectedDataSources);
this.availableDataSources = sources.AllowedDataSources;
this.selectedDataSources = sources.SelectedDataSources;
this.waitingForDataSources = false;
this.StateHasChanged();
}
private async Task EnabledChanged(bool state)
{
this.areDataSourcesEnabled = state;
this.DataSourceOptions.DisableDataSources = !this.areDataSourcesEnabled;
await this.LoadAndApplyFilters();
await this.OptionsChanged();
this.StateHasChanged();
}
private async Task AutoModeChanged(bool state)
{
this.aiBasedSourceSelection = state;
this.DataSourceOptions.AutomaticDataSourceSelection = this.aiBasedSourceSelection;
await this.OptionsChanged();
}
private async Task ValidationModeChanged(bool state)
{
this.aiBasedValidation = state;
this.DataSourceOptions.AutomaticValidation = this.aiBasedValidation;
await this.OptionsChanged();
}
private async Task SelectionChanged(IReadOnlyCollection<IDataSource>? chosenDataSources)
{
this.selectedDataSources = chosenDataSources ?? [];
this.DataSourceOptions.PreselectedDataSourceIds = this.selectedDataSources.Select(ds => ds.Id).ToList();
await this.OptionsChanged();
}
private async Task OptionsChanged()
{
this.internalChange = true;
await this.DataSourceOptionsChanged.InvokeAsync(this.DataSourceOptions);
if(this.AutoSaveAppSettings)
await this.SettingsManager.StoreSettings();
}
private async Task ToggleDataSourceSelection()
{
this.showDataSourceSelection = !this.showDataSourceSelection;
if (this.showDataSourceSelection)
await this.LoadAndApplyFilters();
}
private void HideDataSourceSelection() => this.showDataSourceSelection = false;
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfidenceInfo);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
{
case Event.COLOR_THEME_CHANGED:
this.showDataSourceSelection = false;
this.StateHasChanged();
break;
case Event.RAG_AUTO_DATA_SOURCES_SELECTED:
if(data is IReadOnlyList<DataSourceAgentSelected> aiSelectedDataSources)
this.DataSourcesAISelected = aiSelectedDataSources;
this.StateHasChanged();
break;
}
return Task.CompletedTask;
}
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
{
return Task.FromResult<TResult?>(default);
}
#endregion
#region Implementation of IDisposable
public void Dispose()
{
this.MessageBus.Unregister(this);
}
#endregion
}

View File

@ -0,0 +1,23 @@
namespace AIStudio.Components;
public enum DataSourceSelectionMode
{
/// <summary>
/// The user is selecting data sources for, e.g., the chat.
/// </summary>
/// <remarks>
/// In this case, we have to filter the data sources based on the
/// selected provider and check security requirements.
/// </remarks>
SELECTION_MODE,
/// <summary>
/// The user is configuring the default data sources, e.g., for the chat.
/// </summary>
/// <remarks>
/// In this case, all data sources are available for selection.
/// They get filtered later based on the selected provider and
/// security requirements.
/// </remarks>
CONFIGURATION_MODE,
}

View File

@ -12,6 +12,6 @@
</MudSelect> </MudSelect>
@if (this.AllowOther && this.Value.Equals(this.OtherValue)) @if (this.AllowOther && this.Value.Equals(this.OtherValue))
{ {
<MudTextField T="string" Text="@this.OtherInput" TextChanged="this.OtherInputChanged" Validation="@this.ValidateOther" Label="@this.LabelOther" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Immediate="@true"/> <MudTextField T="string" Text="@this.OtherInput" TextChanged="this.OtherValueChanged" Validation="@this.ValidateOther" Label="@this.LabelOther" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Immediate="@true"/>
} }
</MudStack> </MudStack>

View File

@ -7,17 +7,17 @@
@this.HeaderContent @this.HeaderContent
</div> </div>
} }
<div class="flex-auto overflow-auto"> <div class="flex-auto overflow-auto mb-3">
@this.ChildContent @this.ChildContent
<div @ref="@this.AnchorAfterChildContent"> <div style="max-height: 0.1em;" @ref="@this.AnchorAfterChildContent">
&nbsp; &nbsp;
</div> </div>
</div> </div>
@if (this.FooterContent is not null) @if (this.FooterContent is not null)
{ {
<MudPaper Class="pa-3 border-solid border rounded-lg"> <MudPaper Class="pa-3 mb-3 border-solid border rounded-lg">
@this.FooterContent @this.FooterContent
</MudPaper> </MudPaper>
} }

View File

@ -9,14 +9,6 @@ public partial class InnerScrolling : MSGComponentBase
[Parameter] [Parameter]
public bool FillEntireHorizontalSpace { get; set; } public bool FillEntireHorizontalSpace { get; set; }
/// <summary>
/// Set the height of anything above the scrolling content; usually a header.
/// What we do is calc(100vh - HeaderHeight). Means, you can use multiple measures like
/// 230px - 3em. Default is 3em.
/// </summary>
[Parameter]
public string HeaderHeight { get; set; } = "3em";
[Parameter] [Parameter]
public RenderFragment? HeaderContent { get; set; } public RenderFragment? HeaderContent { get; set; }
@ -35,6 +27,9 @@ public partial class InnerScrolling : MSGComponentBase
[Parameter] [Parameter]
public string? MinWidth { get; set; } public string? MinWidth { get; set; }
[Parameter]
public string Style { get; set; } = string.Empty;
[CascadingParameter] [CascadingParameter]
private MainLayout MainLayout { get; set; } = null!; private MainLayout MainLayout { get; set; } = null!;
@ -55,6 +50,8 @@ public partial class InnerScrolling : MSGComponentBase
#region Overrides of MSGComponentBase #region Overrides of MSGComponentBase
public override string ComponentName => nameof(InnerScrolling);
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{ {
switch (triggeredEvent) switch (triggeredEvent)
@ -74,12 +71,14 @@ public partial class InnerScrolling : MSGComponentBase
#endregion #endregion
private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth};"; private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth}; ";
private string Styles => this.FillEntireHorizontalSpace ? $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); overflow-x: auto; min-width: 0; {this.MinWidthStyle}" : $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight}); flex-shrink: 0; {this.MinWidthStyle}"; private string TerminatedStyles => string.IsNullOrWhiteSpace(this.Style) ? string.Empty : $"{this.Style}; ";
private string Classes => this.FillEntireHorizontalSpace ? $"{this.Class} d-flex flex-column flex-grow-1" : $"{this.Class} d-flex flex-column"; private string Classes => this.FillEntireHorizontalSpace ? $"{this.Class} d-flex flex-column flex-grow-1" : $"{this.Class} d-flex flex-column";
private string Styles => $"flex-grow: 1; overflow: hidden; {this.TerminatedStyles}{this.MinWidthStyle}";
public async Task ScrollToBottom() public async Task ScrollToBottom()
{ {
await this.AnchorAfterChildContent.ScrollIntoViewAsync(this.JsRuntime); await this.AnchorAfterChildContent.ScrollIntoViewAsync(this.JsRuntime);

View File

@ -24,6 +24,8 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
#region Implementation of IMessageBusReceiver #region Implementation of IMessageBusReceiver
public abstract string ComponentName { get; }
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{ {
switch (triggeredEvent) switch (triggeredEvent)

View File

@ -14,7 +14,7 @@ public partial class MudTextList : ComponentBase
public string Icon { get; set; } = Icons.Material.Filled.CheckCircle; public string Icon { get; set; } = Icons.Material.Filled.CheckCircle;
[Parameter] [Parameter]
public string Class { get; set; } = ""; public string Class { get; set; } = string.Empty;
private string Classes => $"mud-text-list {this.Class}"; private string Classes => $"mud-text-list {this.Class}";
} }

View File

@ -1,6 +1,6 @@
namespace AIStudio.Components; namespace AIStudio.Components;
public enum ConfidenceInfoMode public enum PopoverTriggerMode
{ {
BUTTON, BUTTON,
ICON, ICON,

View File

@ -1,4 +1,4 @@
<MudTooltip Placement="Placement.Bottom" Arrow="@true"> <MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
<ChildContent> <ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.FirstPage" Color="Color.Error" Class="mb-3"> <MudChip T="string" Icon="@Icons.Material.Filled.FirstPage" Color="Color.Error" Class="mb-3">
Alpha Alpha

View File

@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class PreviewAlpha : ComponentBase; public partial class PreviewAlpha : ComponentBase
{
[Parameter]
public bool ApplyInnerScrollingFix { get; set; }
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
}

View File

@ -1,4 +1,4 @@
<MudTooltip Placement="Placement.Bottom" Arrow="@true"> <MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
<ChildContent> <ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.HourglassTop" Color="Color.Info" Class="mb-3"> <MudChip T="string" Icon="@Icons.Material.Filled.HourglassTop" Color="Color.Info" Class="mb-3">
Beta Beta

View File

@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class PreviewBeta : ComponentBase; public partial class PreviewBeta : ComponentBase
{
[Parameter]
public bool ApplyInnerScrollingFix { get; set; }
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
}

View File

@ -1,4 +1,4 @@
<MudTooltip Placement="Placement.Bottom" Arrow="@true"> <MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
<ChildContent> <ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.Science" Color="Color.Error" Class="mb-3"> <MudChip T="string" Icon="@Icons.Material.Filled.Science" Color="Color.Error" Class="mb-3">
Experimental Experimental

View File

@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class PreviewExperimental : ComponentBase; public partial class PreviewExperimental : ComponentBase
{
[Parameter]
public bool ApplyInnerScrollingFix { get; set; }
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
}

View File

@ -1,4 +1,4 @@
<MudTooltip Placement="Placement.Bottom" Arrow="@true"> <MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
<ChildContent> <ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.HourglassBottom" Color="Color.Error" Class="mb-3"> <MudChip T="string" Icon="@Icons.Material.Filled.HourglassBottom" Color="Color.Error" Class="mb-3">
Prototype Prototype

View File

@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class PreviewPrototype : ComponentBase; public partial class PreviewPrototype : ComponentBase
{
[Parameter]
public bool ApplyInnerScrollingFix { get; set; }
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
}

View File

@ -1,4 +1,4 @@
<MudTooltip Placement="Placement.Bottom" Arrow="@true"> <MudTooltip Placement="Placement.Bottom" Arrow="@true" Class="@this.Classes">
<ChildContent> <ChildContent>
<MudChip T="string" Icon="@Icons.Material.Filled.VerifiedUser" Color="Color.Success" Class="mb-3"> <MudChip T="string" Icon="@Icons.Material.Filled.VerifiedUser" Color="Color.Success" Class="mb-3">
Release Candidate Release Candidate

View File

@ -2,4 +2,10 @@ using Microsoft.AspNetCore.Components;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class PreviewReleaseCandidate : ComponentBase; public partial class PreviewReleaseCandidate : ComponentBase
{
[Parameter]
public bool ApplyInnerScrollingFix { get; set; }
private string Classes => this.ApplyInnerScrollingFix ? "InnerScrollingFix" : string.Empty;
}

View File

@ -1,5 +1,5 @@
<MudTooltip Text="You can switch between your profiles here"> <MudTooltip Text="You can switch between your profiles here" Placement="Placement.Top">
<MudMenu StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass"> <MudMenu TransformOrigin="@Origin.BottomLeft" AnchorOrigin="Origin.TopLeft" StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass">
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles()) @foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
{ {
<MudMenuItem OnClick="() => this.SelectionChanged(profile)"> <MudMenuItem OnClick="() => this.SelectionChanged(profile)">

View File

@ -15,10 +15,13 @@ public partial class ProfileSelection : ComponentBase
[Parameter] [Parameter]
public string MarginLeft { get; set; } = "ml-3"; public string MarginLeft { get; set; } = "ml-3";
[Parameter]
public string MarginRight { get; set; } = string.Empty;
[Inject] [Inject]
private SettingsManager SettingsManager { get; init; } = null!; private SettingsManager SettingsManager { get; init; } = null!;
private string MarginClass => $"{this.MarginLeft}"; private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
private async Task SelectionChanged(Profile profile) private async Task SelectionChanged(Profile profile)
{ {

View File

@ -1,6 +1,6 @@
@using AIStudio.Settings @using AIStudio.Settings
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" Variant="Variant.Outlined"> <MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="Provider" Class="mb-3 rounded-lg" OuterClass="flex-grow-0" Variant="Variant.Outlined">
@foreach (var provider in this.GetAvailableProviders()) @foreach (var provider in this.GetAvailableProviders())
{ {
<MudSelectItem Value="@provider"/> <MudSelectItem Value="@provider"/>

View File

@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Assistants; using AIStudio.Assistants;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
@ -9,7 +11,7 @@ namespace AIStudio.Components;
public partial class ProviderSelection : ComponentBase public partial class ProviderSelection : ComponentBase
{ {
[CascadingParameter] [CascadingParameter]
public AssistantBase? AssistantBase { get; set; } public AssistantBase<NoComponent>? AssistantBase { get; set; }
[Parameter] [Parameter]
public AIStudio.Settings.Provider ProviderSettings { get; set; } public AIStudio.Settings.Provider ProviderSettings { get; set; }
@ -29,6 +31,7 @@ public partial class ProviderSelection : ComponentBase
await this.ProviderSettingsChanged.InvokeAsync(provider); await this.ProviderSettingsChanged.InvokeAsync(provider);
} }
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
private IEnumerable<AIStudio.Settings.Provider> GetAvailableProviders() private IEnumerable<AIStudio.Settings.Provider> GetAvailableProviders()
{ {
var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.AssistantBase?.Component ?? Tools.Components.NONE); var minimumLevel = this.SettingsManager.GetMinimumConfidenceLevel(this.AssistantBase?.Component ?? Tools.Components.NONE);

View File

@ -2,7 +2,7 @@
<MudTextSwitch Label="Read content from web?" Disabled="@this.AgentIsRunning" @bind-Value="@this.showWebContentReader" LabelOn="Show web content options" LabelOff="Hide web content options" /> <MudTextSwitch Label="Read content from web?" Disabled="@this.AgentIsRunning" @bind-Value="@this.showWebContentReader" LabelOn="Show web content options" LabelOff="Hide web content options" />
@if (this.showWebContentReader) @if (this.showWebContentReader)
{ {
<MudTextSwitch Label="Cleanup content by using a LLM agent?" @bind-Value="@this.useContentCleanerAgent" Validation="@this.ValidateProvider" Disabled="@this.AgentIsRunning" LabelOn="The content is cleaned using an LLM agent: the main content is extracted, advertisements and other irrelevant things are attempted to be removed; relative links are attempted to be converted into absolute links so that they can be used." LabelOff="No content cleaning" /> <MudTextSwitch Label="Cleanup content by using an LLM agent?" @bind-Value="@this.useContentCleanerAgent" Validation="@this.ValidateProvider" Disabled="@this.AgentIsRunning" LabelOn="The content is cleaned using an LLM agent: the main content is extracted, advertisements and other irrelevant things are attempted to be removed; relative links are attempted to be converted into absolute links so that they can be used." LabelOff="No content cleaning" />
<MudStack Row="@true" AlignItems="@AlignItems.Baseline" Class="mb-3"> <MudStack Row="@true" AlignItems="@AlignItems.Baseline" Class="mb-3">
<MudTextField T="string" Label="URL from which to load the content" @bind-Value="@this.providedURL" Validation="@this.ValidateURL" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Link" Placeholder="https://..." HelperText="Loads the content from your URL. Does not work when the content is hidden behind a paywall." Variant="Variant.Outlined" Immediate="@true" Disabled="@this.AgentIsRunning"/> <MudTextField T="string" Label="URL from which to load the content" @bind-Value="@this.providedURL" Validation="@this.ValidateURL" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Link" Placeholder="https://..." HelperText="Loads the content from your URL. Does not work when the content is hidden behind a paywall." Variant="Variant.Outlined" Immediate="@true" Disabled="@this.AgentIsRunning"/>
<MudButton Disabled="@(!this.IsReady || this.AgentIsRunning)" Variant="Variant.Filled" Size="Size.Large" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Download" OnClick="() => this.LoadFromWeb()"> <MudButton Disabled="@(!this.IsReady || this.AgentIsRunning)" Variant="Variant.Filled" Size="Size.Large" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Download" OnClick="() => this.LoadFromWeb()">

View File

@ -59,11 +59,7 @@ public partial class ReadWebContent : ComponentBase
if(this.PreselectContentCleanerAgent) if(this.PreselectContentCleanerAgent)
this.useContentCleanerAgent = true; this.useContentCleanerAgent = true;
if (this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions) this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_TEXT_CONTENT_CLEANER, this.ProviderSettings.Id, true);
this.providerSettings = this.SettingsManager.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.SettingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider);
else
this.providerSettings = this.ProviderSettings;
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }

View File

@ -4,6 +4,7 @@
Text="@this.Directory" Text="@this.Directory"
Label="@this.Label" Label="@this.Label"
ReadOnly="@true" ReadOnly="@true"
Validation="@this.Validation"
Adornment="Adornment.Start" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Folder" AdornmentIcon="@Icons.Material.Filled.Folder"
UserAttributes="@SPELLCHECK_ATTRIBUTES" UserAttributes="@SPELLCHECK_ATTRIBUTES"

View File

@ -1,4 +1,5 @@
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -21,6 +22,9 @@ public partial class SelectDirectory : ComponentBase
[Parameter] [Parameter]
public string DirectoryDialogTitle { get; set; } = "Select Directory"; public string DirectoryDialogTitle { get; set; } = "Select Directory";
[Parameter]
public Func<string, string?> Validation { get; set; } = _ => null;
[Inject] [Inject]
private SettingsManager SettingsManager { get; init; } = null!; private SettingsManager SettingsManager { get; init; } = null!;

View File

@ -0,0 +1,17 @@
<MudStack Row="@true" Spacing="3" Class="mb-3" StretchItems="StretchItems.None" AlignItems="AlignItems.Center">
<MudTextField
T="string"
Text="@this.File"
Label="@this.Label"
ReadOnly="@true"
Validation="@this.Validation"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.AttachFile"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenFileDialog">
Choose File
</MudButton>
</MudStack>

View File

@ -0,0 +1,64 @@
using AIStudio.Settings;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class SelectFile : ComponentBase
{
[Parameter]
public string File { get; set; } = string.Empty;
[Parameter]
public EventCallback<string> FileChanged { get; set; }
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string FileDialogTitle { get; set; } = "Select File";
[Parameter]
public Func<string, string?> Validation { get; set; } = _ => null;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
public RustService RustService { get; set; } = null!;
[Inject]
protected ILogger<SelectDirectory> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
await base.OnInitializedAsync();
}
#endregion
private void InternalFileChanged(string file)
{
this.File = file;
this.FileChanged.InvokeAsync(file);
}
private async Task OpenFileDialog()
{
var response = await this.RustService.SelectFile(this.FileDialogTitle, string.IsNullOrWhiteSpace(this.File) ? null : this.File);
this.Logger.LogInformation($"The user selected the file '{response.SelectedFilePath}'.");
if (!response.UserCancelled)
this.InternalFileChanged(response.SelectedFilePath);
}
}

View File

@ -1,32 +0,0 @@
@using AIStudio.Settings
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.CalendarToday" HeaderText="Assistant: Agenda Options">
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="Preselect agenda options?" LabelOn="Agenda options are preselected" LabelOff="No agenda options are preselected" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectOptions = updatedState)" OptionHelp="When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often."/>
<ConfigurationText OptionDescription="Preselect a name?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Tag" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectName)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectName = updatedText)" />
<ConfigurationText OptionDescription="Preselect a topic?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.EventNote" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectTopic)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectTopic = updatedText)" />
<ConfigurationText OptionDescription="Preselect an objective?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Flag" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectObjective)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectObjective = updatedText)" />
<ConfigurationText OptionDescription="Preselect a moderator?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Person3" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectModerator)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectModerator = updatedText)" />
<ConfigurationText OptionDescription="Preselect a duration?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Schedule" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectDuration)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectDuration = updatedText)" />
<ConfigurationText OptionDescription="Preselect a start time?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Schedule" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectStartTime)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectStartTime = updatedText)" />
<ConfigurationOption OptionDescription="Preselect whether the participants should get to know each other" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="Participants should get to know each other" LabelOff="Participants do not need to get to know each other" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectIntroduceParticipants)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectIntroduceParticipants = updatedState)" />
<ConfigurationSelect OptionDescription="Preselect the number of participants" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectNumberParticipants)" Data="@ConfigurationSelectDataFactory.GetNumberParticipantsData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectNumberParticipants = selectedValue)" OptionHelp="How many participants should be preselected?"/>
<ConfigurationOption OptionDescription="Preselect whether the participants should actively involved" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="Participants should be actively involved" LabelOff="Participants do not need to be actively involved" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectActiveParticipation)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectActiveParticipation = updatedState)" />
<ConfigurationOption OptionDescription="Preselect whether the meeting is virtual" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="Meeting is virtual" LabelOff="Meeting is in person" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectIsMeetingVirtual)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectIsMeetingVirtual = updatedState)" />
<ConfigurationText OptionDescription="Preselect a location?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.MyLocation" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectLocation)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectLocation = updatedText)" />
<ConfigurationOption OptionDescription="Preselect whether there is a joint dinner" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="There is a joint dinner" LabelOff="There is no joint dinner" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectJointDinner)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectJointDinner = updatedState)" />
<ConfigurationOption OptionDescription="Preselect whether there is a social event" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="There is a social event" LabelOff="There is no social event" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectSocialActivity)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectSocialActivity = updatedState)" />
<ConfigurationOption OptionDescription="Preselect whether participants needs to arrive and depart" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" LabelOn="Participants need to arrive and depart" LabelOff="Participants do not need to arrive and depart" State="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectArriveAndDepart)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Agenda.PreselectArriveAndDepart = updatedState)" />
<ConfigurationSlider T="int" OptionDescription="Preselect the approx. lunch time" Min="30" Max="120" Step="5" Unit="minutes" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Value="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectLunchTime)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectLunchTime = updatedValue)" />
<ConfigurationSlider T="int" OptionDescription="Preselect the approx. break time" Min="10" Max="60" Step="5" Unit="minutes" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Value="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectBreakTime)" ValueUpdate="@(updatedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectBreakTime = updatedValue)" />
<ConfigurationSelect OptionDescription="Preselect the agenda language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage)" Data="@ConfigurationSelectDataFactory.GetCommonLanguagesTranslationData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage = selectedValue)" OptionHelp="Which agenda language should be preselected?"/>
@if (this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage is CommonLanguages.OTHER)
{
<ConfigurationText OptionDescription="Preselect another agenda language" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.Agenda.PreselectedOtherLanguage = updatedText)"/>
}
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.AGENDA_ASSISTANT" Data="@this.AvailableLLMProvidersFunc()" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile = selectedValue)" OptionHelp="Would you like to preselect one of your profiles?"/>
</MudPaper>
</ExpansionPanel>

View File

@ -1,3 +0,0 @@
namespace AIStudio.Components.Settings;
public partial class SettingsPanelAgenda : SettingsPanelBase;

Some files were not shown because too many files have changed in this diff Show More