Compare commits

...
178 Commits
Author SHA1 Message Date
Thorsten Sommer 3ab8e7324e Prepared release v0.9.36 (#351) 2025-03-15 11:52:52 +01:00
Thorsten Sommer cf97fa888c Undo debugging measures (#350)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
2025-03-15 11:37:09 +01:00
Thorsten Sommer dd23cfa1ff Raised the importance of the z-index (#349)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2025-03-15 11:18:47 +01:00
Thorsten Sommer d6ead37102 Fixed #346 (#348) 2025-03-15 11:09:34 +01:00
Thorsten Sommer 686bb2ccb3 Updated the run command in the build documentation (#347)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
2025-03-14 17:29:22 +01:00
Thorsten Sommer 69d64f8b1f Allow build artifacts for debugging (#343)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2025-03-13 14:08:55 +01:00
Thorsten Sommer c3ab0ebb38 Enable devtools for debugging (#342) 2025-03-13 14:02:48 +01:00
Thorsten Sommer 6af3cf4213 Updated workflow for pipeline (#341)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
2025-03-13 10:20:55 +01:00
Thorsten Sommer 52c8052ce6 Updated metadata for v0.9.35 (#340)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2025-03-13 09:48:38 +01:00
Thorsten Sommer 471536cc8f Pipeline: update artifact deletion commands to use PowerShell syntax (#339)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
2025-03-13 08:49:28 +01:00
Thorsten Sommer e5ce3263bc Pipeline: update artifact deletion commands to use PowerShell syntax (#338)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
2025-03-13 08:34:15 +01:00
Thorsten Sommer 264b6dd716 Pipeline: add steps to delete previous artifacts to prevent caching issues (#337)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
2025-03-13 08:20:14 +01:00
Thorsten Sommer d8ac2f3d49 Updated metadata for release v0.9.35 (#336)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2025-03-12 20:09:29 +01:00
Thorsten Sommer 64a7c9ab53 Fixed the static web assets (#335)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2025-03-12 20:02:40 +01:00
Thorsten Sommer ee61425153 Prepared release v0.9.35 (#334)
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2025-03-12 19:36:05 +01:00
Thorsten Sommer ce8d84a12a Migrate to .NET 9 & MudBlazor 8 (#333)
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
2025-03-12 19:12:56 +01:00
Thorsten Sommer 2e8444b5e3 Code maintenance (#332)
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
2025-03-12 18:05:27 +01:00
Thorsten Sommer 5cc6e141c2 GitHub workflow: added tag check for metadata processing (#331)
Build and Release / Read metadata (push) Waiting to run
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Blocked by required conditions
Build and Release / Build app (linux-arm64) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
2025-03-12 13:18:28 +01:00
Thorsten Sommer 850d56cfcf Enhance build & release workflow (#330) 2025-03-12 13:10:28 +01:00
Peer Schütt d399b025a9 Added changelog for newest features of #327 (#329) 2025-03-12 11:58:43 +01:00
Peer Schütt 200964422d Included a settings icon to every assistant that lets you edit the settings (#327) 2025-03-12 11:31:01 +01:00
Thorsten Sommer 8a23b54e56 Added next changelog (#328) 2025-03-12 09:06:56 +01:00
Thorsten Sommer eccf65f0a6 Prepared release v0.9.34 (#326) 2025-03-11 14:12:36 +01:00
Thorsten Sommer 2429788ccc Fixed secret retrieval for connection testing while adding an ERI data source (#325) 2025-03-11 13:57:47 +01:00
Thorsten Sommer 0125f48f61 Prepared release v0.9.33 (#324) 2025-03-11 09:26:22 +01:00
Thorsten Sommer 94028b2c06 Fixed a bug concerning authentication methods for added ERI servers (#323) 2025-03-11 09:11:06 +01:00
Thorsten Sommer c5acce7278 Added next changelog (#321) 2025-03-10 08:46:51 +01:00
Thorsten Sommer 4eb51f947a Prepared release v0.9.32 (#320) 2025-03-08 21:24:39 +01:00
Thorsten Sommer e11211d215 Updated the code contributions section on the supporter page (#319) 2025-03-08 21:13:18 +01:00
Thorsten Sommer 032c7ee56e Added the retrieval process section for ERI data sources (#318) 2025-03-08 21:04:17 +01:00
Thorsten Sommer 030990ee90 Improved data security for chat threads (#317) 2025-03-08 20:13:08 +01:00
Thorsten Sommer 371731ac13 Upgraded dependencies (#316) 2025-03-08 14:38:38 +01:00
Thorsten Sommer 99622cec48 Fixed the default data source selection when replacing the chat (#315) 2025-03-08 14:23:24 +01:00
Thorsten Sommer 6a4a7dc0d6 Improved the augmentation & generation of RAG (#314) 2025-03-08 13:56:38 +01:00
Thorsten Sommer df01ce188e Improved RAG issue handling (#313) 2025-03-08 11:14:20 +01:00
Thorsten Sommer 3bfca45e11 Update changelog (#312) 2025-03-08 09:56:21 +01:00
Peer SchüttandPeer Schütt ccaf2e894a Added a new panel in the about section regarding the Github repos (#310)
Co-authored-by: Peer Schütt <peer.schuett@dlr.de>
2025-03-07 09:24:53 +01:00
Thorsten Sommer 1d22232005 ERI: use camel case for JSON (#311) 2025-03-06 21:02:21 +01:00
Thorsten Sommer a66d88c989 Updated v0.9.31 metadata (#307) 2025-03-03 16:35:22 +01:00
Thorsten Sommer 2e84f10a97 Fixed the conversion of log paths (FileSpec) to String (#306) 2025-03-03 15:14:09 +01:00
Thorsten Sommer dbc2b4f6bc Prepared release v0.9.31 (#305) 2025-02-27 20:26:36 +01:00
Thorsten Sommer 9d2b63bbaa Added paths to log files in the "About" dialog for easier support (#304) 2025-02-27 20:17:51 +01:00
Thorsten Sommer 277dc73468 Added DeepSeek as provider (#303) 2025-02-27 12:43:19 +01:00
Thorsten Sommer ccac2b04ac Resolved warnings regarding redundant switch arms (#302) 2025-02-27 08:42:51 +01:00
Thorsten Sommer 0c4b94c527 Fixed warnings when compiling for Windows (#300) 2025-02-27 08:30:18 +01:00
Thorsten Sommer 37a9046be4 Added GWDG provider (#301) 2025-02-26 15:34:50 +01:00
Thorsten Sommer b984b4432e Added Helmholtz (aka "Blablador") as provider (#299) 2025-02-26 13:22:01 +01:00
Thorsten Sommer 5e3b632eee Extended code analyzers (#293) 2025-02-25 19:58:17 +01:00
Thorsten Sommer 99dac520cb Prepared release v0.9.30 (#298) 2025-02-24 20:58:35 +01:00
Thorsten Sommer e21e30ce8c Implemented the new models API for the Anthropic provider (#297) 2025-02-24 20:52:32 +01:00
Thorsten Sommer 5a077e1bdb Fixed the date of the release (#296) 2025-02-24 15:06:51 +01:00
Thorsten Sommer 8dd5b3bab8 Prepared release v0.9.29 (#295) 2025-02-24 15:02:24 +01:00
Thorsten Sommer 03431b85b4 Fixed a layout bug within the chat page (#294) 2025-02-24 12:48:41 +01:00
Thorsten Sommer 9699754ec7 Prepared release v0.9.29 (#292) 2025-02-23 16:20:02 +01:00
Thorsten Sommer 64656d3605 Improved the readability of descriptions inside the ERI server assistant (#291) 2025-02-23 15:21:06 +01:00
Thorsten Sommer 9bd79bd3a0 Improved data security by enforcing provider filtering (#290) 2025-02-23 15:05:29 +01:00
Thorsten Sommer bfc9f2ea1d Added a retrieval context validation agent (#289) 2025-02-22 20:51:06 +01:00
Thorsten Sommer 96e6372fcd Added a common augmentation interface & default implementation (#288) 2025-02-18 11:24:43 +01:00
Thorsten Sommer f01cf498e2 Refactored the RAG process (#287) 2025-02-17 16:51:26 +01:00
Thorsten Sommer 77d427610b Execute retrieval processes (#286) 2025-02-17 14:12:46 +01:00
Thorsten Sommer 7c59aa11fe Refactored DI usage for CreateFromProviderAsync (#285) 2025-02-17 12:41:27 +01:00
Thorsten Sommer 98810ce884 Added an agent to select the appropriate data sources for any prompt (#284) 2025-02-17 12:33:34 +01:00
Thorsten Sommer 657fda4961 Fixed a bug in which 'APP_SETTINGS' appeared in the "send to" menu (#283) 2025-02-15 15:53:58 +01:00
Thorsten Sommer f4780939fc Integrated data sources into any chat (#282) 2025-02-15 15:41:12 +01:00
Thorsten Sommer 954cf44939 Added retrieval integration, part I (#281) 2025-02-10 18:49:22 +01:00
Thorsten Sommer e973b51f75 Fixed Tauri TLS build issue caused by OpenSSL dependency (#280) 2025-02-09 19:01:51 +01:00
Thorsten Sommer c9474c55b7 Prepared release v0.9.28 (#279) 2025-02-09 18:06:24 +01:00
Thorsten Sommer c0b7016c3a Added the assistant option to select the default app provider (#278) 2025-02-09 16:59:26 +01:00
Thorsten Sommer 78315195cb Improved build script (#277) 2025-02-09 16:25:40 +01:00
Thorsten Sommer 6040e88dad Updated the supporter page (#276) 2025-02-09 15:04:07 +01:00
Thorsten Sommer ad3d8cbb3a Updated OpenSSL dependency to fix potential security issues (#275) 2025-02-09 13:10:20 +01:00
Thorsten Sommer c836bd7f33 Added an information view to all data sources (#273) 2025-02-09 12:36:37 +01:00
Thorsten Sommer 9d71978feb Prepared release v0.9.27 (#272) 2025-01-21 19:28:09 +01:00
Thorsten Sommer 940459ca46 Fixed an issue with tooltips at the bottom of the app (#271) 2025-01-21 19:22:26 +01:00
Thorsten Sommer 09d67bc1c2 Fixed the workspace name after re-rendering the chat component (#270) 2025-01-21 19:12:36 +01:00
Thorsten Sommer 1681405fb9 Fixed an issue with the workspace title not being updated (#269) 2025-01-21 18:58:20 +01:00
Thorsten Sommer be408a6aee Fixed message bus issues (#268) 2025-01-21 15:36:22 +01:00
Thorsten Sommer cf975f2a6c Removed the "send to" button from the ERI server assistant (#267) 2025-01-21 14:20:03 +01:00
Thorsten Sommer 1df1d3f328 Updated changelog (#266) 2025-01-21 13:52:10 +01:00
Thorsten Sommer ed14525390 Fixed hostname validation message for ERI data sources (#265) 2025-01-21 13:49:44 +01:00
Thorsten Sommer 14caf550ec Improved the inner content scrolling to use the entire space available (#263) 2025-01-21 13:40:47 +01:00
Thorsten Sommer f50e041640 Updated introduction (#264) 2025-01-21 09:13:16 +01:00
Thorsten Sommer ebe00812e2 Prepared release v0.9.26 (#262) 2025-01-13 20:20:40 +01:00
Thorsten Sommer ae72c50236 Fixed the embedding dialog to store the selected embedding model (#261) 2025-01-13 20:10:58 +01:00
Thorsten Sommer 48874d617a Added a confirmation dialog for deleting ERI server presets (#260) 2025-01-13 19:58:52 +01:00
Thorsten Sommer 63be312bb4 Configure data sources (#259) 2025-01-13 19:51:26 +01:00
Thorsten Sommer 06f66fdab2 Refactored the settings page (#258) 2025-01-05 15:11:15 +01:00
Thorsten Sommer b9a65d9ca0 Prepared release v0.9.25 (#257) 2025-01-04 19:38:06 +01:00
Thorsten Sommer e4b1ea32fb Fixed chat loading by assistants (#256) 2025-01-04 19:27:45 +01:00
Thorsten Sommer e9975a5fbc Fixed the chat scrolling behavior after toggling the workspaces (#255) 2025-01-04 18:27:23 +01:00
Thorsten Sommer 2ef4d9d53f Added xAI as a new provider (#254) 2025-01-04 15:06:08 +01:00
Thorsten Sommer 33a2728644 Refactored network code (#253) 2025-01-04 14:11:32 +01:00
Thorsten Sommer a54c0bdbbf Prepared release v0.9.24 (#252) 2025-01-04 12:44:02 +01:00
Thorsten Sommer d6521850e9 Improved the stream handling for all providers (#251) 2025-01-04 12:37:49 +01:00
Thorsten Sommer 258bc7a338 Fixed a streaming bug (#250) 2025-01-04 12:30:05 +01:00
Thorsten Sommer 187663bbf2 Added the option to stop the current generation (#249) 2025-01-03 23:13:56 +01:00
Thorsten Sommer 8060fc01dd Added button to edit last user message (#248) 2025-01-03 21:58:31 +01:00
Thorsten Sommer b2ca49ab92 Added a button to regenerate the last AI response (#247) 2025-01-03 21:18:27 +01:00
Thorsten Sommer 5e445f09fa Implemented removal of chat messages (#246) 2025-01-03 18:01:22 +01:00
Thorsten Sommer 6f5aecd92b Fixed Ubuntu arm build pipeline (#245) 2025-01-02 21:29:20 +01:00
Thorsten Sommer 317d3c9eec Prepared release v0.9.23 (#244) 2025-01-02 20:47:44 +01:00
Thorsten Sommer 0acb3a20a2 Improved the navbar handling when configured as "never expands" (#243) 2025-01-02 20:26:04 +01:00
Thorsten Sommer 3b93841982 Fixed dark mode code blocks (#242) 2025-01-02 15:24:15 +01:00
Thorsten Sommer 38a45955fb Improved profile and system prompt handling (#241) 2025-01-02 14:50:54 +01:00
Thorsten Sommer caec3bfd2c Improved chat UI (#240) 2025-01-02 13:16:47 +01:00
Thorsten Sommer 12b3d6fc3d Fixed OpenAI o models (#239) 2025-01-01 20:11:42 +01:00
Thorsten Sommer 21927754b6 Upgraded dependencies (#238) 2025-01-01 18:05:26 +01:00
Thorsten Sommer 2131bf1817 Improved the save operation of settings by using a temporary file (#237) 2025-01-01 17:15:04 +01:00
Thorsten Sommer 8cbef49d89 Add ERI server assistant (#231) 2025-01-01 15:49:27 +01:00
Luc Bologna cdf717ad5f Update .gitignore and improve build.nu script 2024-12-26 14:39:29 +01:00
Thorsten Sommer 02b6f526da Added CFF (#234) 2024-12-13 15:16:26 +01:00
Thorsten Sommer a365539282 Renamed EDI to ERI (#233) 2024-12-13 14:33:38 +01:00
Thorsten Sommer 7250b270f5 Prepared release v0.9.22 (#230) 2024-12-04 12:04:39 +01:00
Thorsten Sommer d437d08640 Updated readme (#229) 2024-12-04 11:53:49 +01:00
Thorsten Sommer 61f7a3dd72 Fixed bugs related to the workspaces component (#228) 2024-12-04 11:44:12 +01:00
Thorsten Sommer 55d7895f58 Added preview feature configuration (#227) 2024-12-03 21:02:37 +01:00
Thorsten Sommer e2859b3d76 Define the writer mode as experimental preview (#226) 2024-12-03 15:52:45 +01:00
Thorsten Sommer 0a951ead3e Added the writer mode experiment (#167) 2024-12-03 15:32:38 +01:00
Thorsten Sommer 432ae30fdf Updated RAG tasks (#225) 2024-12-03 15:28:21 +01:00
Thorsten Sommer 799112eb9d Added embedding configurations (#224) 2024-12-03 15:24:40 +01:00
Thorsten Sommer f806ee28c5 Updated readme (#223) 2024-11-24 10:19:05 +01:00
Thorsten Sommer 32ff143b21 Added configuration for preview feature visibility (#222) 2024-11-24 10:11:56 +01:00
Thorsten Sommer 90a1f681c5 Readme: added link to o1 issue (#221) 2024-11-24 09:41:59 +01:00
Thorsten Sommer 99fe9f3a4f Updated RAG tasks (#220) 2024-11-23 13:35:42 +01:00
Thorsten Sommer cf8a9800f3 Prepared release v0.9.21 (#219) 2024-11-23 13:25:24 +01:00
Thorsten Sommer f57e971f80 Upgraded dependencies (#218) 2024-11-23 13:19:43 +01:00
Thorsten Sommer 0d104f5abc Remember chat provider (#217) 2024-11-23 13:04:02 +01:00
Thorsten Sommer ba6b8d2b3c Fixed auto-save when sending assistant results to a new chat (#216) 2024-11-23 10:54:58 +01:00
Thorsten Sommer 1c72c4d6ec Migrated to use the shared random instance (#214) 2024-11-16 12:20:04 +01:00
Thorsten Sommer 4ca5aba58f Fixed workspace titles (#213) 2024-11-16 11:59:09 +01:00
Thorsten Sommer c4d2574508 Prepared release v0.9.20 (#212) 2024-11-15 21:50:12 +01:00
Thorsten Sommer ceac9cefe5 Fixed bugs related to storing and loading chats (#211) 2024-11-15 21:22:57 +01:00
Thorsten Sommer ebe2ad62c6 Prepared release v0.9.19 (#210) 2024-11-14 07:03:01 +01:00
Thorsten Sommer 9c0dea9927 Updated supporters (#209) 2024-11-14 06:54:19 +01:00
Thorsten Sommer d2a2d658e6 Fixed a bug regarding hostnames of self-hosted providers (#208) 2024-11-14 06:39:35 +01:00
Thorsten Sommer d82427ba56 Upgraded dependencies (#207) 2024-11-14 06:24:29 +01:00
Thorsten Sommer 386121d198 Updated RAG news (#206) 2024-11-13 20:42:41 +01:00
Thorsten Sommer f9245d52c2 Added provider metadata regarding embeddings (#205) 2024-11-13 20:33:52 +01:00
Thorsten Sommer 9fa2a1a135 Updated readme (#203) 2024-11-10 10:41:20 +01:00
Thorsten Sommer 69288699b3 Prepared v0.9.18 (#202) 2024-11-09 22:16:37 +01:00
Thorsten Sommer 826d15729b Added the Google provider (#201) 2024-11-09 22:04:00 +01:00
Thorsten Sommer 2ae40c59a3 Added Groq provider (#200) 2024-11-09 20:13:14 +01:00
Thorsten Sommer 119100bbce Cleaned bias descriptions (#199) 2024-11-09 19:37:54 +01:00
Thorsten Sommer f92fab24b3 Updated RAG tasks (#198) 2024-11-08 22:49:09 +01:00
Thorsten Sommer 671abf3b07 Updated RAG tasks (#196) 2024-11-05 21:42:46 +01:00
Thorsten Sommer d0a366258a Refactored runtime (#192) 2024-11-05 21:39:21 +01:00
Thorsten Sommer 58d7478f3a Updated RAG tasks (#195) 2024-11-05 14:35:54 +01:00
Thorsten Sommer e8216b70a5 Added new Anthropic model claude-3-5-heiku-20241022 (#194) 2024-11-05 08:43:16 +01:00
Thorsten Sommer 152d5f9b4f Added PR regarding runtime refactoring (#193) 2024-11-04 20:48:25 +01:00
Thorsten Sommer f1104c5e09 Created RAG task list (#190) 2024-11-03 21:39:29 +01:00
Thorsten Sommer 3ccb097705 Updated news section (#189) 2024-11-03 20:52:31 +01:00
Thorsten Sommer b9416e66c3 Prepared release v0.9.17 (#188) 2024-11-03 12:17:04 +01:00
Thorsten Sommer 9f576e8572 Added new Anthropic models (#187) 2024-11-03 12:06:54 +01:00
Thorsten Sommer 629613f75a Prepared release v0.9.16 (#186) 2024-11-02 23:13:30 +01:00
Thorsten Sommer abbb8c1650 Fixed the appearance of the "no diff" message for text diffs (#185) 2024-11-02 23:03:07 +01:00
Thorsten Sommer d2ac4e26b0 Added workspace display options (#184) 2024-11-02 22:53:02 +01:00
Thorsten Sommer bd97c0fb4b Fixed the "send to" issue, where you could select the bias-of-the-day (#183) 2024-11-02 11:55:09 +01:00
Thorsten Sommer 8bd54e6d61 Added second supporter (#182) 2024-11-02 11:40:17 +01:00
Thorsten Sommer e419c5561b Improved the layout of the app window (#181) 2024-11-02 11:12:54 +01:00
Thorsten Sommer 24a73e92e1 Added media (#180) 2024-10-31 10:48:56 +01:00
Thorsten Sommer 832e054dbf Updated news (#179) 2024-10-28 20:19:38 +01:00
Thorsten Sommer 4703ab04ab Updated news (#178) 2024-10-28 16:53:18 +01:00
Thorsten Sommer c1d5be63cc Prepared release v0.9.15 (#177) 2024-10-28 16:10:49 +01:00
Thorsten Sommer 5d3ea3d57a Upgraded dependencies (#176) 2024-10-28 15:59:36 +01:00
Thorsten Sommer 217b9c4db3 Bias of the day (#173) 2024-10-28 15:41:00 +01:00
Thorsten Sommer e9c805f0df Updated news (#174) 2024-10-21 20:41:35 +02:00
Thorsten Sommer 11475ea4af Added bias names (#172) 2024-10-21 18:21:08 +02:00
Thorsten Sommer 46a7e98141 Defined bias catalog (#166) 2024-10-20 12:35:03 +02:00
Thorsten Sommer 0d6ccbd237 Prepare release v0.9.14 (#170) 2024-10-18 11:23:33 +02:00
Thorsten Sommer efb30cad87 Upgraded dependencies (#169) 2024-10-18 10:40:19 +02:00
Thorsten Sommer fe33867cb6 Updated news (#168) 2024-10-14 19:58:45 +02:00
Thorsten Sommer 1b1989e8eb Improved build introductions (#165) 2024-10-14 19:23:40 +02:00
Thorsten Sommer afa5df894e Add news section (#164) 2024-10-11 08:58:26 +02:00
Thorsten Sommer c8d67b1e4c Removed previous run configuration for JetBrains Rider (#163) 2024-10-11 08:33:19 +02:00
Thorsten Sommer e717b84fb4 Added @richard-stanton as the first supporter (#162) 2024-10-11 08:30:13 +02:00
Thorsten Sommer 2463c85b77 Updated build documentation (#161) 2024-10-11 07:43:45 +02:00
Thorsten Sommer cbf34580b3 Fixed media files to use LFS (#160) 2024-10-11 07:15:24 +02:00
Thorsten Sommer e60f815d77 Fixed LFS tracking for woff2, afpub, afdesign, afphoto files (#159) 2024-10-08 07:53:15 +02:00
979 changed files with 27824 additions and 4443 deletions

No files matched your search

+4
View File
@@ -11,3 +11,7 @@ media/**/*.pptx filter=lfs diff=lfs merge=lfs -text
media/**/*.png filter=lfs diff=lfs merge=lfs -text
media/**/*.ico filter=lfs diff=lfs merge=lfs -text
media/**/*.icns filter=lfs diff=lfs merge=lfs -text
*.woff2 filter=lfs diff=lfs merge=lfs -text
*.afpub filter=lfs diff=lfs merge=lfs -text
*.afdesign filter=lfs diff=lfs merge=lfs -text
*.afphoto filter=lfs diff=lfs merge=lfs -text
+5 -5
View File
@@ -2,13 +2,13 @@
* @MindWorkAI/maintainer
# The release team is responsible for anything inside the .github directory, such as workflows, actions, and issue templates:
/.github/ @MindWorkAI/release
# The release team is responsible for the update directory:
/.updates/ @MindWorkAI/release
/.github/ @MindWorkAI/release @SommerEngineering
# Our Rust experts are responsible for the Rust codebase:
/runtime/ @MindWorkAI/rust-experts
# 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
+52 -7
View File
@@ -1,6 +1,8 @@
name: Build and Release
on:
push:
branches:
- main
tags:
- "v*.*.*"
@@ -46,6 +48,7 @@ jobs:
echo "version=${version}" >> "$GITHUB_OUTPUT"
- name: Check tag vs. metadata version
if: startsWith(github.ref, 'refs/tags/v')
run: |
# Ensure, that the tag matches the version in the metadata file:
if [ "${GITHUB_REF}" != "refs/tags/${FORMATTED_VERSION}" ]; then
@@ -291,6 +294,35 @@ jobs:
} else {
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"
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/msi/MindWork AI Studio*msi.zip*"
- 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"
rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio*nsis.zip*"
- 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)
if: matrix.platform != 'windows-latest'
@@ -315,7 +347,7 @@ jobs:
cargo tauri build --target ${{ matrix.rust_target }} --bundles ${{ matrix.tauri_bundle }}
- 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
with:
name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }})
@@ -326,7 +358,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- 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
with:
name: MindWork AI Studio (Windows - MSI ${{ matrix.dotnet_runtime }})
@@ -337,7 +369,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- 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
with:
name: MindWork AI Studio (Windows - NSIS ${{ matrix.dotnet_runtime }})
@@ -348,7 +380,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- 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
with:
name: MindWork AI Studio (Linux - deb ${{ matrix.dotnet_runtime }})
@@ -358,7 +390,7 @@ jobs:
retention-days: ${{ env.RETENTION_INTERMEDIATE_ASSETS }}
- 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
with:
name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }})
@@ -370,7 +402,7 @@ jobs:
build_linux_arm64:
name: Build app (linux-arm64)
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
needs: read_metadata
env:
SKIP: false # allows disabling this long-running job temporarily
@@ -506,6 +538,14 @@ jobs:
if: ${{ steps.linux_arm_cache.outputs.cache-hit != 'true' && env.SKIP != 'true' }}
run: |
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
if: ${{ env.SKIP != 'true' }}
@@ -531,6 +571,9 @@ jobs:
shell: /bin/bash
commands: |
# Delete all previous artifacts, which may exist due to caching:
rm -f result/target/aarch64-unknown-linux-gnu/release/bundle/deb/mind-work-ai-studio_*.deb
export HOME=/root
export CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
source "$HOME/.cargo/env"
@@ -553,7 +596,7 @@ jobs:
mv ${{ steps.build-linux-arm.outputs.image }} $RUNNER_TEMP/linux_arm_qemu_cache.img
- 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
with:
name: MindWork AI Studio (Linux - deb linux-arm64)
@@ -566,6 +609,7 @@ jobs:
name: Prepare & create release
runs-on: ubuntu-latest
needs: [build_main, read_metadata, build_linux_arm64]
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Create artifact directory
run: mkdir -p $GITHUB_WORKSPACE/artifacts
@@ -723,6 +767,7 @@ jobs:
name: Publish release
runs-on: ubuntu-latest
needs: [read_metadata, create_release]
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
+1
View File
@@ -147,3 +147,4 @@ orleans.codegen.cs
**/.idea/**/dynamic.xml
**/.idea/**/uiDesigner.xml
**/.idea/**/dbnavigator.xml
**/.vs
+22
View File
@@ -0,0 +1,22 @@
# This CITATION.cff file was generated with cffinit.
# Visit https://bit.ly/cffinit to generate yours today!
cff-version: 1.2.0
title: AI Studio
message: >-
When you want to cite AI Studio in your scientific work,
please use these metadata.
type: software
authors:
- given-names: Thorsten
family-names: Sommer
email: thorsten.sommer@dlr.de
affiliation: Deutsches Zentrum für Luft- und Raumfahrt (DLR)
orcid: 'https://orcid.org/0000-0002-3264-9934'
- name: Open Source Community
repository-code: 'https://github.com/MindWorkAI/AI-Studio'
url: 'https://mindworkai.org/'
keywords:
- LLM
- AI
- Orchestration
+1 -1
View File
@@ -6,7 +6,7 @@ FSL-1.1-MIT
## Notice
Copyright 2024 Thorsten Sommer
Copyright 2025 Thorsten Sommer
## Terms and Conditions
+65 -9
View File
@@ -1,15 +1,66 @@
# MindWork AI Studio
Are you new here? [Read here](#what-is-ai-studio) what AI Studio is.
## News
Things we are currently working on:
- Since November 2024: Work on RAG (integration of your data and files) has begun. We will support the integration of local and external data sources. We need to implement the following runtime (Rust) and app (.NET) steps:
- [x] ~~Runtime: Restructuring the code into meaningful modules (PR [#192](https://github.com/MindWorkAI/AI-Studio/pull/192))~~
- [x] ~~Define the [External Retrieval Interface (ERI)](https://github.com/MindWorkAI/ERI) as a contract for integrating arbitrary external data (PR [#1](https://github.com/MindWorkAI/ERI/pull/1))~~
- [x] ~~App: Metadata for providers (which provider offers embeddings?) (PR [#205](https://github.com/MindWorkAI/AI-Studio/pull/205))~~
- [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: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~
- [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~
- [ ] 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)
- [ ] App: Implement external embedding providers
- [ ] App: Implement the process to vectorize one local file using embeddings
- [ ] Runtime: Integration of the vector database [LanceDB](https://github.com/lancedb/lancedb)
- [ ] App: Implement the continuous process of vectorizing data
- [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~
- [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~
- [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)~~.
Other News:
- October 2024: We've found the first two financial supporters. Huge thanks to `richard-stanton` and `peerschuett` for backing the project. Thanks for having the courage to be the first to support us.
- October 2024: The [German Aerospace Center (DLR)](https://en.wikipedia.org/wiki/German_Aerospace_Center) ([Website](https://www.dlr.de/en)) will use AI Studio at least within the scope of one project and will also contribute to its further development. This is great news.
Features we have recently released:
- v0.9.25: Added [xAI](https://x.ai/) as a new provider. xAI provides their Grok models for generating content.
- v0.9.23: Added support for OpenAI `o` models (`o1`, `o1-mini`, `o3`, etc.); added also an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant as a preview feature behind the RAG feature flag. Your own ERI server can be used to gain access to, e.g., your enterprise data from within AI Studio.
- v0.9.22: Added options for preview features; added embedding provider configuration for RAG (preview) and writer mode (experimental preview).
- v0.9.18: Added the new Anthropic Heiku model; added Groq and Google Gemini as provider options.
- v0.9.17: Added the new Anthropic model `claude-3-5-sonnet-20241022`.
- v0.9.16: Added workspace display options & improved the layout of the app window.
- v0.9.15: Added the bias-of-the-day assistant. Tells you about a cognitive bias every day.
- v0.9.13: You can use `ollama` providers secured with API keys.
## What is AI Studio?
![MindWork AI Studio - Home](documentation/AI%20Studio%20Home.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:**
- **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), and self-hosted models using [llama.cpp](https://github.com/ggerganov/llama.cpp), [ollama](https://github.com/ollama/ollama), [LM Studio](https://lmstudio.ai/), or [Fireworks](https://fireworks.ai/). Support for Google Gemini, and [Replicate](https://replicate.com/) is planned.
- **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.
- **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.
- **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.
@@ -25,16 +76,21 @@ We offer various ways you can support the project:
- **One-Time Contributions**: Make a one-time donation and have your name or company logo included in the app as a gesture of our gratitude.
For companies, sponsoring MindWork AI Studio is not only a way to support innovation but also a valuable opportunity for public relations and marketing. Your company's name and logo will be featured prominently, showcasing your commitment to using cutting-edge AI tools and enhancing your reputation as an innovative enterprise.
To view all available tiers, please visit our [GitHub Sponsors page](https://github.com/sponsors/MindWorkAI).
Your support, whether big or small, keeps the wheels turning and is deeply appreciated ❤️.
## Planned Features
Here's an exciting look at some of the features we're planning to add to MindWork AI Studio in future releases:
- **More providers**: We plan to add support for additional LLM providers, such as Google Gemini, giving you more options to choose from.
- **System prompts**: Integration of a system prompt library will allow you to control the behavior of the LLM with predefined prompts, ensuring consistency and efficiency.
- **Text replacement for better privacy**: Define keywords that will be replaced in your chats before sending content to the provider, enhancing your privacy.
- **Advanced interactions**: We're full of ideas for advanced interactions tailored for specific use cases, whether in a business context or for writers and other professionals.
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'll be able to integrate your data into AI Studio, like your PDF or Office files, or your Markdown notes.
- **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.
- **Useful assistants:** We'll develop more assistants for everyday tasks.
- **Writing mode:** We're integrating a writing mode to help you create extensive works, like comprehensive project proposals, tenders, or your next fantasy novel.
- **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.
- **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 🤩.
-21
View File
@@ -1,21 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="AI Studio" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/MindWork AI Studio/bin/Debug/net8.0/osx-arm64/mindworkAIStudio.dll" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/MindWork AI Studio" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="RUNTIME_TYPE" value="coreclr" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/MindWork AI Studio/MindWork AI Studio.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
</component>
-7
View File
@@ -1,7 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run Dev" type="CompoundRunConfigurationType">
<toRun name="AI Studio" type="DotNetProject" />
<toRun name="Tauri Dev" type="ShConfigurationType" />
<method v="2" />
</configuration>
</component>
+8
View File
@@ -2,6 +2,8 @@
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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceCodeRules", "SourceCodeRules\SourceCodeRules\SourceCodeRules.csproj", "{0976C1CB-D499-4C86-8ADA-B7A7A4DE0BF8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
EndGlobal
+9
View File
@@ -1,7 +1,16 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=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/=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/=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/=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/=tauri_0027s/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
-4
View File
@@ -1,4 +0,0 @@
*.woff2 filter=lfs diff=lfs merge=lfs -text
*.afpub filter=lfs diff=lfs merge=lfs -text
*.afdesign filter=lfs diff=lfs merge=lfs -text
*.afphoto filter=lfs diff=lfs merge=lfs -text
+35 -11
View File
@@ -1,19 +1,37 @@
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.Services;
// ReSharper disable MemberCanBePrivate.Global
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 ThreadSafeRandom RNG { get; init; } = rng;
protected ILogger<AgentBase> Logger { get; init; } = logger;
/// <summary>
/// Represents the type or category of this agent.
/// </summary>
@@ -60,24 +78,30 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
Blocks = [],
};
protected DateTimeOffset AddUserRequest(ChatThread thread, string request)
protected UserRequest AddUserRequest(ChatThread thread, string request)
{
var time = DateTimeOffset.Now;
var lastUserPrompt = new ContentText
{
Text = request,
};
thread.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
{
Text = request,
},
Content = lastUserPrompt,
});
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)
return;
@@ -103,6 +127,6 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// 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);
}
}
@@ -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;
}
}
@@ -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 [];
}
}
@@ -1,24 +1,17 @@
using AIStudio.Chat;
using AIStudio.Settings;
using AIStudio.Tools.Services;
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> answers = new();
#region Overrides of AgentBase
public override Settings.Provider? ProviderSettings { get; set; }
public override AIStudio.Settings.Provider? ProviderSettings { get; set; }
protected override Type Type => Type.SYSTEM;
@@ -72,8 +65,8 @@ public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsM
return EMPTY_BLOCK;
var thread = this.CreateChatThread(this.SystemPrompt(sourceURL));
var time = this.AddUserRequest(thread, text.Text);
await this.AddAIResponseAsync(thread, time);
var userRequest = this.AddUserRequest(thread, text.Text);
await this.AddAIResponseAsync(thread, userRequest.UserPrompt, userRequest.Time);
var answer = thread.Blocks[^1];
this.answers.Add(answer);
@@ -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;
@@ -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;
@@ -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; }
}
+2
View File
@@ -13,6 +13,7 @@
<link rel="icon" type="image/png" href="favicon.png"/>
<link href="system/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="system/MudBlazor.Markdown/MudBlazor.Markdown.min.css" rel="stylesheet" />
<link href="system/CodeBeam.MudBlazor.Extensions/MudExtensions.min.css" rel="stylesheet" />
<link href="app.css" rel="stylesheet" />
<HeadOutlet/>
<script src="diff.js"></script>
@@ -24,6 +25,7 @@
<script src="boot.js"></script>
<script src="system/MudBlazor/MudBlazor.min.js"></script>
<script src="system/MudBlazor.Markdown/MudBlazor.Markdown.min.js"></script>
<script src="system/CodeBeam.MudBlazor.Extensions/MudExtensions.min.js"></script>
<script src="app.js"></script>
</body>
@@ -1,5 +1,5 @@
@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.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"/>
@@ -1,10 +1,11 @@
using System.Text;
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Agenda;
public partial class AssistantAgenda : AssistantBaseCore
public partial class AssistantAgenda : AssistantBaseCore<SettingsDialogAgenda>
{
public override Tools.Components Component => Tools.Components.AGENDA_ASSISTANT;
@@ -106,7 +107,7 @@ public partial class AssistantAgenda : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputContent = string.Empty;
this.contentLines.Clear();
@@ -1,117 +1,144 @@
@using AIStudio.Chat
@inherits AssistantLowerBase
@typeparam TSettings
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
@(this.Title)
</MudText>
<div class="inner-scrolling-context">
<InnerScrolling HeaderHeight="6em">
<ChildContent>
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" Class="pr-2">
<MudText Typo="Typo.body1" Align="Align.Justify" Class="mb-6">
@this.Description
</MudText>
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
@(this.Title)
</MudText>
@if (this.Body is not null)
{
<CascadingValue Value="@this">
@this.Body
</CascadingValue>
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle">
@this.SubmitText
</MudButton>
}
</MudForm>
<Issues IssuesData="@(this.inputIssues)"/>
<InnerScrolling>
<ChildContent>
<MudForm @ref="@(this.form)" @bind-IsValid="@(this.inputIsValid)" @bind-Errors="@(this.inputIssues)" FieldChanged="@this.TriggerFormChange" Class="pr-2">
<MudGrid Class="mb-2">
<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>
@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.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@(this.resultingContentBlock.Role)" Type="@(this.resultingContentBlock.ContentType)" Time="@(this.resultingContentBlock.Time)" Content="@(this.resultingContentBlock.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))
{
<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>().OrderBy(n => n.Name().Length))
{
if (assistant is Components.NONE || this.Component == assistant)
continue;
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
}
@foreach (var button in this.FooterButtons)
{
switch (button)
@if (this.Body is not null)
{
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;
<CascadingValue Value="@this">
@this.Body
</CascadingValue>
<MudButton Disabled="@this.SubmitDisabled" Variant="Variant.Filled" Class="mb-3" OnClick="() => this.SubmitAction()" Style="@this.SubmitButtonStyle">
@this.SubmitText
</MudButton>
}
</MudForm>
<Issues IssuesData="@(this.inputIssues)"/>
case ButtonData buttonData:
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
break;
@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>
case SendToButton sendToButton:
<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 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>().OrderBy(n => n.Name().Length))
@foreach (var assistant in Enum.GetValues<Components>().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length))
{
if(assistant is Components.NONE || sendToButton.Self == assistant)
continue;
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, sendToButton)">
<MudMenuItem OnClick="() => this.SendToAssistant(assistant, new())">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
break;
}
}
}
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
Copy result
</MudButton>
<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="ConfidenceInfoMode.BUTTON" LLMProvider="@this.providerSettings.UsedLLMProvider"/>
}
@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;
@if (this.AllowProfiles && this.ShowProfileSelection)
{
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.currentProfile"/>
}
</MudStack>
</FooterContent>
</InnerScrolling>
case ButtonData buttonData:
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
break;
case SendToButton sendToButton:
<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, sendToButton)">
@assistant.Name()
</MudMenuItem>
}
</MudMenu>
break;
}
}
@if (this.ShowCopyResult)
{
<MudButton Variant="Variant.Filled" StartIcon="@Icons.Material.Filled.ContentCopy" OnClick="() => this.CopyToClipboard()">
Copy result
</MudButton>
}
@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>
@@ -1,18 +1,26 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using RustService = AIStudio.Tools.RustService;
using MudBlazor.Utilities;
using Timer = System.Timers.Timer;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Assistants;
public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver, IDisposable
public abstract partial class AssistantBase<TSettings> : AssistantLowerBase, IMessageBusReceiver, IDisposable where TSettings : IComponent
{
[Inject]
protected SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
[Inject]
protected IJSRuntime JsRuntime { get; init; } = null!;
@@ -29,7 +37,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
protected NavigationManager NavigationManager { get; init; } = null!;
[Inject]
protected ILogger<AssistantBase> Logger { get; init; } = null!;
protected ILogger<AssistantBase<TSettings>> Logger { get; init; } = null!;
[Inject]
private MudTheme ColorTheme { get; init; } = null!;
@@ -37,10 +45,6 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
[Inject]
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 Description { get; }
@@ -55,38 +59,47 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
_ => string.Empty,
};
protected abstract void ResetFrom();
protected abstract void ResetForm();
protected abstract bool MightPreselectValues();
protected abstract string SubmitText { get; }
protected abstract Func<Task> SubmitAction { get; }
protected virtual bool SubmitDisabled => false;
private protected virtual RenderFragment? Body => null;
protected virtual bool ShowResult => true;
protected virtual bool ShowEntireChatThread => false;
protected virtual bool AllowProfiles => true;
protected virtual bool ShowProfileSelection => true;
protected virtual bool ShowDedicatedProgress => false;
protected virtual bool ShowSendTo => true;
protected virtual bool ShowCopyResult => true;
protected virtual bool ShowReset => true;
protected virtual ChatThread ConvertToChatThread => this.chatThread ?? new();
protected virtual IReadOnlyList<IButtonData> FooterButtons => [];
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
protected AIStudio.Settings.Provider providerSettings;
protected MudForm? form;
protected bool inputIsValid;
protected Profile currentProfile = Profile.NO_PROFILE;
protected ChatThread? chatThread;
protected IContent? lastUserPrompt;
private readonly Timer formChangeTimer = new(TimeSpan.FromSeconds(1.6));
private ContentBlock? resultingContentBlock;
private string[] inputIssues = [];
private bool isProcessing;
@@ -95,6 +108,13 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
protected override async Task OnInitializedAsync()
{
this.formChangeTimer.AutoReset = false;
this.formChangeTimer.Elapsed += async (_, _) =>
{
this.formChangeTimer.Stop();
await this.OnFormChange();
};
this.MightPreselectValues();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
@@ -127,6 +147,8 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(AssistantBase<TSettings>);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
@@ -155,43 +177,89 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
return null;
}
private void TriggerFormChange(FormFieldChangedEventArgs _)
{
this.formChangeTimer.Stop();
this.formChangeTimer.Start();
}
/// <summary>
/// This method is called after any form field has changed.
/// </summary>
/// <remarks>
/// This method is called after a delay of 1.6 seconds. This is to prevent
/// the method from being called too often. This method is called after
/// the user has stopped typing or selecting options.
/// </remarks>
protected virtual Task OnFormChange() => Task.CompletedTask;
/// <summary>
/// Add an issue to the UI.
/// </summary>
/// <param name="issue">The issue to add.</param>
protected void AddInputIssue(string issue)
{
Array.Resize(ref this.inputIssues, this.inputIssues.Length + 1);
this.inputIssues[^1] = issue;
this.inputIsValid = false;
this.StateHasChanged();
}
protected void CreateChatThread()
{
this.chatThread = new()
{
SelectedProvider = this.providerSettings.Id,
SelectedProfile = this.AllowProfiles ? this.currentProfile.Id : Profile.NO_PROFILE.Id,
SystemPrompt = this.SystemPrompt,
WorkspaceId = Guid.Empty,
ChatId = Guid.NewGuid(),
Name = string.Empty,
Name = $"Assistant - {this.Title}",
Seed = this.RNG.Next(),
SystemPrompt = !this.AllowProfiles ? this.SystemPrompt :
$"""
{this.SystemPrompt}
{this.currentProfile.ToSystemPrompt()}
""",
Blocks = [],
};
}
protected Guid CreateChatThread(Guid workspaceId, string name)
{
var chatId = Guid.NewGuid();
this.chatThread = new()
{
SelectedProvider = this.providerSettings.Id,
SelectedProfile = this.AllowProfiles ? this.currentProfile.Id : Profile.NO_PROFILE.Id,
SystemPrompt = this.SystemPrompt,
WorkspaceId = workspaceId,
ChatId = chatId,
Name = name,
Seed = this.RNG.Next(),
Blocks = [],
};
return chatId;
}
protected DateTimeOffset AddUserRequest(string request)
protected DateTimeOffset AddUserRequest(string request, bool hideContentFromUser = false)
{
var time = DateTimeOffset.Now;
this.lastUserPrompt = new ContentText
{
Text = request,
};
this.chatThread!.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
HideFromUser = hideContentFromUser,
Role = ChatRole.USER,
Content = new ContentText
{
Text = request,
},
Content = this.lastUserPrompt,
});
return time;
}
protected async Task<string> AddAIResponseAsync(DateTimeOffset time)
protected async Task<string> AddAIResponseAsync(DateTimeOffset time, bool hideContentFromUser = false)
{
var aiText = new ContentText
{
@@ -206,16 +274,22 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
ContentType = ContentType.TEXT,
Role = ChatRole.AI,
Content = aiText,
HideFromUser = hideContentFromUser,
};
this.chatThread?.Blocks.Add(this.resultingContentBlock);
if (this.chatThread is not null)
{
this.chatThread.Blocks.Add(this.resultingContentBlock);
this.chatThread.SelectedProvider = this.providerSettings.Id;
}
this.isProcessing = true;
this.StateHasChanged();
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, this.lastUserPrompt, this.chatThread);
this.isProcessing = false;
this.StateHasChanged();
@@ -237,9 +311,18 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
return icon;
}
private Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
protected async Task OpenSettingsDialog()
{
var contentToSend = sendToButton.UseResultingContentBlockData switch
var dialogParameters = new DialogParameters();
await this.DialogService.ShowAsync<TSettings>("Open Settings", dialogParameters, DialogOptions.FULLSCREEN);
}
protected Task SendToAssistant(Tools.Components destination, SendToButton sendToButton)
{
if (!destination.AllowSendTo())
return Task.CompletedTask;
var contentToSend = sendToButton == default ? string.Empty : sendToButton.UseResultingContentBlockData switch
{
false => sendToButton.GetText(),
true => this.resultingContentBlock?.Content switch
@@ -253,7 +336,9 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
switch (destination)
{
case Tools.Components.CHAT:
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, this.ConvertToChatThread);
var convertedChatThread = this.ConvertToChatThread;
convertedChatThread = convertedChatThread with { SelectedProvider = this.providerSettings.Id };
MessageBus.INSTANCE.DeferMessage(this, sendToData.Event, convertedChatThread);
break;
default:
@@ -273,7 +358,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
await this.JsRuntime.ClearDiv(RESULT_DIV_ID);
await this.JsRuntime.ClearDiv(AFTER_RESULT_DIV_ID);
this.ResetFrom();
this.ResetForm();
this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.inputIsValid = false;
@@ -301,6 +386,7 @@ public abstract partial class AssistantBase : ComponentBase, IMessageBusReceiver
public void Dispose()
{
this.MessageBus.Unregister(this);
this.formChangeTimer.Dispose();
}
#endregion
@@ -7,7 +7,7 @@ namespace AIStudio.Assistants;
// 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;
@@ -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";
}
@@ -0,0 +1,14 @@
@attribute [Route(Routes.ASSISTANT_BIAS)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogAssistantBias>
<MudText Typo="Typo.body1">
<b>Links:</b>
</MudText>
<MudList T="string" Class="mb-6">
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://en.wikipedia.org/wiki/List_of_cognitive_biases">Wikipedia list of cognitive biases</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://commons.wikimedia.org/wiki/File:Cognitive_Bias_Codex_With_Definitions_1-2,_an_Extension_of_the_work_of_John_Manoogian_by_Brian_Rene_Morrissette.png">Extended bias poster</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://betterhumans.pub/cognitive-bias-cheat-sheet-55a472476b18">Blog post of Buster Benson: "Cognitive bias cheat sheet"</MudListItem>
</MudList>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelecting())" @bind-Value="@this.selectedTargetLanguage" ValidateSelection="@this.ValidateTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Target language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom target language" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
@@ -0,0 +1,165 @@
using System.Text;
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
using AIStudio.Settings.DataModel;
namespace AIStudio.Assistants.BiasDay;
public partial class BiasOfTheDayAssistant : AssistantBaseCore<SettingsDialogAssistantBias>
{
public override Tools.Components Component => Tools.Components.BIAS_DAY_ASSISTANT;
protected override string Title => "Bias of the Day";
protected override string Description =>
"""
Learn about a different cognitive bias every day. You can also ask the LLM your questions. The idea behind
"Bias of the Day" is based on work by Buster Benson, John Manoogian III, and Brian Rene Morrissette. Buster
Benson grouped the biases, and the original texts come from Wikipedia. Brian Rene Morrissette condensed them
into a shorter version. Finally, John Manoogian III created the original poster based on Benson's work and
Morrissette's texts. Thorsten Sommer compared all texts for integration into AI Studio with the current Wikipedia
versions, updated them, and added source references. The idea of learning about one bias each day based on John's
poster comes from Drew Nelson.
""";
protected override string SystemPrompt => $"""
You are a friendly, helpful expert on cognitive bias. You studied psychology and
have a lot of experience. You explain a bias every day. Today's bias belongs to
the category: "{this.biasOfTheDay.Category.ToName()}". We have the following
thoughts on this category:
{this.biasOfTheDay.Category.GetThoughts()}
Today's bias is:
{this.biasOfTheDay.Description}
{this.SystemPromptSources()}
Important: you use the following language: {this.SystemPromptLanguage()}. Please
ask the user a personal question at the end to encourage them to think about
this bias.
""";
protected override IReadOnlyList<IButtonData> FooterButtons => [];
protected override string SubmitText => "Show me the bias of the day";
protected override Func<Task> SubmitAction => this.TellBias;
protected override bool ShowSendTo => false;
protected override bool ShowCopyResult => false;
protected override bool ShowReset => false;
protected override void ResetForm()
{
if (!this.MightPreselectValues())
{
this.selectedTargetLanguage = CommonLanguages.AS_IS;
this.customTargetLanguage = string.Empty;
}
}
protected override bool MightPreselectValues()
{
if (this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)
{
this.selectedTargetLanguage = this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedTargetLanguage;
this.customTargetLanguage = this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedOtherLanguage;
return true;
}
return false;
}
private Bias biasOfTheDay = BiasCatalog.NONE;
private CommonLanguages selectedTargetLanguage = CommonLanguages.AS_IS;
private string customTargetLanguage = string.Empty;
private string? ValidateTargetLanguage(CommonLanguages language)
{
if(language is CommonLanguages.AS_IS)
return "Please select a target language for the bias.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string SystemPromptSources()
{
var sb = new StringBuilder();
if (this.biasOfTheDay.Links.Count > 0)
{
sb.AppendLine();
sb.AppendLine("Please share the following sources with the user as a Markdown list:");
foreach (var link in this.biasOfTheDay.Links)
sb.AppendLine($"- {link}");
sb.AppendLine();
}
return sb.ToString();
}
private string SystemPromptLanguage()
{
if(this.selectedTargetLanguage is CommonLanguages.OTHER)
return this.customTargetLanguage;
return this.selectedTargetLanguage.Name();
}
private async Task TellBias()
{
bool useDrawnBias = false;
if(this.SettingsManager.ConfigurationData.BiasOfTheDay.RestrictOneBiasPerDay)
{
if(this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn == DateOnly.FromDateTime(DateTime.Now))
{
var biasChat = new LoadChat
{
WorkspaceId = KnownWorkspaces.BIAS_WORKSPACE_ID,
ChatId = this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId,
};
if (WorkspaceBehaviour.IsChatExisting(biasChat))
{
MessageBus.INSTANCE.DeferMessage(this, Event.LOAD_CHAT, biasChat);
this.NavigationManager.NavigateTo(Routes.CHAT);
return;
}
else
useDrawnBias = true;
}
}
await this.form!.Validate();
if (!this.inputIsValid)
return;
this.biasOfTheDay = useDrawnBias ?
BiasCatalog.ALL_BIAS[this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId] :
BiasCatalog.GetRandomBias(this.SettingsManager.ConfigurationData.BiasOfTheDay.UsedBias);
var chatId = this.CreateChatThread(KnownWorkspaces.BIAS_WORKSPACE_ID, this.biasOfTheDay.Name);
this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayId = this.biasOfTheDay.Id;
this.SettingsManager.ConfigurationData.BiasOfTheDay.BiasOfTheDayChatId = chatId;
this.SettingsManager.ConfigurationData.BiasOfTheDay.DateLastBiasDrawn = DateOnly.FromDateTime(DateTime.Now);
await this.SettingsManager.StoreSettings();
var time = this.AddUserRequest(
"""
Please tell me about the bias of the day.
""", true);
// Start the AI response without waiting for it to finish:
_ = this.AddAIResponseAsync(time);
await this.SendToAssistant(Tools.Components.CHAT, default);
}
}
@@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_CODING)]
@inherits AssistantBaseCore
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogCoding>
<MudExpansionPanels Class="mb-3">
@for (var contextIndex = 0; contextIndex < this.codingContexts.Count; contextIndex++)
@@ -1,8 +1,10 @@
using System.Text;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Coding;
public partial class AssistantCoding : AssistantBaseCore
public partial class AssistantCoding : AssistantBaseCore<SettingsDialogCoding>
{
public override Tools.Components Component => Tools.Components.CODING_ASSISTANT;
@@ -31,8 +33,8 @@ public partial class AssistantCoding : AssistantBaseCore
protected override string SubmitText => "Get Support";
protected override Func<Task> SubmitAction => this.GetSupport;
protected override void ResetFrom()
protected override void ResetForm()
{
this.codingContexts.Clear();
this.compilerMessages = string.Empty;
@@ -1,5 +1,5 @@
<MudTextField T="string" @bind-Text="@this.CodingContext.Id" AdornmentIcon="@Icons.Material.Filled.Numbers" Adornment="Adornment.Start" Label="(Optional) Identifier" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudStack Row="@true" Class="mb-3">
<MudSelect T="CommonCodingLanguages" @bind-Value="@this.CodingContext.Language" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Language" Variant="Variant.Outlined" Margin="Margin.Dense">
@foreach (var language in Enum.GetValues<CommonCodingLanguages>())
{
@@ -1,5 +1,5 @@
@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" />
@if (this.provideHistory)
@@ -1,10 +1,11 @@
using System.Text;
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.EMail;
public partial class AssistantEMail : AssistantBaseCore
public partial class AssistantEMail : AssistantBaseCore<SettingsDialogWritingEMails>
{
public override Tools.Components Component => Tools.Components.EMAIL_ASSISTANT;
@@ -27,13 +28,13 @@ public partial class AssistantEMail : AssistantBaseCore
protected override string SubmitText => "Create email";
protected override Func<Task> SubmitAction => this.CreateMail;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputBulletPoints = string.Empty;
this.bulletPointsLines.Clear();
@@ -0,0 +1,9 @@
namespace AIStudio.Assistants.ERI;
public enum AllowedLLMProviders
{
NONE,
ANY,
SELF_HOSTED,
}
@@ -0,0 +1,13 @@
namespace AIStudio.Assistants.ERI;
public static class AllowedLLMProvidersExtensions
{
public static string Description(this AllowedLLMProviders provider) => 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.SELF_HOSTED => "Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider",
_ => "Unknown option was selected"
};
}
@@ -0,0 +1,349 @@
@attribute [Route(Routes.ASSISTANT_ERI)]
@using AIStudio.Settings.DataModel
@using MudExtensions
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogERIServer>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
You can imagine it like this: Hypothetically, when Wikipedia implemented the ERI, it would vectorize
all pages using an embedding method. All of Wikipedia’s data would remain with Wikipedia, including the
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.
</MudJustifiedText>
<MudText Typo="Typo.body1">
<b>Related links:</b>
</MudText>
<MudList T="string" Class="mb-6">
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://github.com/MindWorkAI/ERI">ERI repository with example implementation in .NET and C#</MudListItem>
<MudListItem T="string" Icon="@Icons.Material.Filled.Link" Target="_blank" Href="https://mindworkai.org/swagger-ui.html">Interactive documentation aka Swagger UI</MudListItem>
</MudList>
<PreviewPrototype/>
<div class="mb-6"></div>
<MudText Typo="Typo.h4" Class="mb-3">
ERI server presets
</MudText>
<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
you are responsible for multiple ERI servers.
</MudJustifiedText>
@if(this.SettingsManager.ConfigurationData.ERI.ERIServers.Count is 0)
{
<MudText Typo="Typo.body1" Class="mb-3">
You have not yet added any ERI server presets.
</MudText>
}
else
{
<MudList Disabled="@this.AreServerPresetsBlocked" T="DataERIServer" Class="mb-1" SelectedValue="@this.selectedERIServer" SelectedValueChanged="@this.SelectedERIServerChanged">
@foreach (var server in this.SettingsManager.ConfigurationData.ERI.ERIServers)
{
<MudListItem T="DataERIServer" Icon="@Icons.Material.Filled.Settings" Value="@server">
@server.ServerName
</MudListItem>
}
</MudList>
}
<MudStack Row="@true" Class="mt-1">
<MudButton Disabled="@this.AreServerPresetsBlocked" OnClick="@this.AddERIServer" Variant="Variant.Filled" Color="Color.Primary">
Add ERI server preset
</MudButton>
<MudButton OnClick="@this.RemoveERIServer" Disabled="@(this.AreServerPresetsBlocked || this.IsNoneERIServerSelected)" Variant="Variant.Filled" Color="Color.Error">
Delete this server preset
</MudButton>
</MudStack>
@if(this.AreServerPresetsBlocked)
{
<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.
</MudJustifiedText>
}
<MudText Typo="Typo.h4" Class="mb-3 mt-6">
Auto save
</MudText>
<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
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?
</MudJustifiedText>
@if(this.AreServerPresetsBlocked)
{
<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.
</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" />
<hr style="width: 100%; border-width: 0.25ch;" class="mt-6"/>
<MudText Typo="Typo.h4" Class="mt-6 mb-1">
Common ERI server settings
</MudText>
<MudTextField T="string" Disabled="@this.IsNoneERIServerSelected" @bind-Text="@this.serverName" Validation="@this.ValidateServerName" Immediate="@true" Label="ERI server name" HelperText="Please give your ERI server a name that provides information about the data source and/or its intended purpose. The name will be displayed to users in AI Studio." Counter="60" MaxLength="60" Variant="Variant.Outlined" Margin="Margin.Normal" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3" OnKeyUp="() => this.ServerNameWasChanged()"/>
<MudTextField T="string" Disabled="@this.IsNoneERIServerSelected" @bind-Text="@this.serverDescription" Validation="@this.ValidateServerDescription" Immediate="@true" Label="ERI server description" HelperText="Please provide a brief description of your ERI server. Describe or explain what your ERI server does and what data it uses for this purpose. This description will be shown to users in AI Studio." Counter="512" MaxLength="512" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
<MudStack Row="@true" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="ProgrammingLanguages" @bind-Value="@this.selectedProgrammingLanguage" AdornmentIcon="@Icons.Material.Filled.Code" Adornment="Adornment.Start" Label="Programming language" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateProgrammingLanguage">
@foreach (var language in Enum.GetValues<ProgrammingLanguages>())
{
<MudSelectItem Value="@language">@language.Name()</MudSelectItem>
}
</MudSelect>
@if (this.selectedProgrammingLanguage is ProgrammingLanguages.OTHER)
{
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.otherProgrammingLanguage" Validation="@this.ValidateOtherLanguage" Label="Other language" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="ERIVersion" @bind-Value="@this.selectedERIVersion" Label="ERI specification version" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateERIVersion">
@foreach (var version in Enum.GetValues<ERIVersion>())
{
<MudSelectItem Value="@version">@version</MudSelectItem>
}
</MudSelect>
<MudButton Variant="Variant.Outlined" Size="Size.Small" Disabled="@(!this.selectedERIVersion.WasSpecificationSelected() || this.IsNoneERIServerSelected)" Href="@this.selectedERIVersion.SpecificationURL()" Target="_blank">
<MudIcon Icon="@Icons.Material.Filled.Link" Class="mr-2"/> Download specification
</MudButton>
</MudStack>
<MudText Typo="Typo.h4" Class="mt-9 mb-3">
Data source settings
</MudText>
<MudStack Row="@false" Spacing="1" Class="mb-3">
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="DataSources" @bind-Value="@this.selectedDataSource" AdornmentIcon="@Icons.Material.Filled.Dataset" Adornment="Adornment.Start" Label="Data source" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateDataSource" SelectedValuesChanged="@this.DataSourceWasChanged">
@foreach (var dataSource in Enum.GetValues<DataSources>())
{
<MudSelectItem Value="@dataSource">@dataSource.Name()</MudSelectItem>
}
</MudSelect>
@if (this.selectedDataSource is DataSources.CUSTOM)
{
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.otherDataSource" Validation="@this.ValidateOtherDataSource" Label="Describe your data source" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
}
</MudStack>
@if(this.selectedDataSource > DataSources.FILE_SYSTEM)
{
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.dataSourceProductName" Label="Data source: product name" Validation="@this.ValidateDataSourceProductName" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES" Class="mb-3"/>
}
@if (this.NeedHostnamePort())
{
<div class="mb-3">
<MudStack Row="@true">
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.dataSourceHostname" Label="Data source: hostname" Validation="@this.ValidateHostname" Variant="Variant.Outlined" Margin="Margin.Dense" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudNumericField Disabled="@this.IsNoneERIServerSelected" Label="Data source: port" Immediate="@true" Min="1" Max="65535" Validation="@this.ValidatePort" @bind-Value="@this.dataSourcePort" Variant="Variant.Outlined" Margin="Margin.Dense" OnKeyUp="() => this.DataSourcePortWasTyped()"/>
</MudStack>
@if (this.dataSourcePort < 1024)
{
<MudText Typo="Typo.body2">
<b>Warning:</b> Ports below 1024 are reserved for system services. Your ERI server need to run with elevated permissions (root user).
</MudText>
}
</div>
}
<MudText Typo="Typo.h4" Class="mt-9 mb-3">
Authentication settings
</MudText>
<MudStack Row="@false" Spacing="1" Class="mb-1">
<MudSelectExtended
T="Auth"
Disabled="@this.IsNoneERIServerSelected"
ShrinkLabel="@true"
MultiSelection="@true"
MultiSelectionTextFunc="@this.GetMultiSelectionAuthText"
SelectedValues="@this.selectedAuthenticationMethods"
Validation="@this.ValidateAuthenticationMethods"
SelectedValuesChanged="@this.AuthenticationMethodWasChanged"
Label="Authentication method(s)"
Variant="Variant.Outlined"
Margin="Margin.Dense">
@foreach (var authMethod in Enum.GetValues<Auth>())
{
<MudSelectItemExtended Value="@authMethod">@authMethod.Name()</MudSelectItemExtended>
}
</MudSelectExtended>
<MudTextField Disabled="@this.IsNoneERIServerSelected" T="string" @bind-Text="@this.authDescription" Label="@this.AuthDescriptionTitle()" Validation="@this.ValidateAuthDescription" Variant="Variant.Outlined" Margin="Margin.Normal" Lines="3" AutoGrow="@true" MaxLines="6" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
</MudStack>
@if (this.selectedAuthenticationMethods.Contains(Auth.KERBEROS))
{
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="OperatingSystem" @bind-Value="@this.selectedOperatingSystem" Label="Operating system on which your ERI will run" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateOperatingSystem" Class="mb-1">
@foreach (var os in Enum.GetValues<OperatingSystem>())
{
<MudSelectItem Value="@os">@os.Name()</MudSelectItem>
}
</MudSelect>
}
<MudText Typo="Typo.h4" Class="mt-11 mb-3">
Data protection settings
</MudText>
<MudSelect Disabled="@this.IsNoneERIServerSelected" T="AllowedLLMProviders" @bind-Value="@this.allowedLLMProviders" Label="Allowed LLM providers for this data source" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateAllowedLLMProviders" Class="mb-1">
@foreach (var option in Enum.GetValues<AllowedLLMProviders>())
{
<MudSelectItem Value="@option">@option.Description()</MudSelectItem>
}
</MudSelect>
<MudText Typo="Typo.h4" Class="mt-11 mb-3">
Embedding settings
</MudText>
<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
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
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.
</MudJustifiedText>
<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
or data types. For example, one embedding for texts, another for images, and a third for videos, etc.
</MudJustifiedText>
@if (!this.IsNoneERIServerSelected)
{
<MudTable Items="@this.embeddings" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col/>
<col style="width: 34em;"/>
<col style="width: 34em;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Type</MudTh>
<MudTh Style="text-align: left;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.EmbeddingName</MudTd>
<MudTd>@context.EmbeddingType</MudTd>
<MudTd Style="text-align: left;">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditEmbedding(context)">
Edit
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteEmbedding(context)">
Delete
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
@if (this.embeddings.Count == 0)
{
<MudText Typo="Typo.h6" Class="mt-3">No embedding methods configured yet.</MudText>
}
}
<MudButton Disabled="@this.IsNoneERIServerSelected" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddEmbedding">
Add Embedding Method
</MudButton>
<MudText Typo="Typo.h4" Class="mt-6 mb-1">
Data retrieval settings
</MudText>
<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.
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
the process that best suits their circumstances.
</MudJustifiedText>
@if (!this.IsNoneERIServerSelected)
{
<MudTable Items="@this.retrievalProcesses" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col/>
<col style="width: 34em;"/>
</ColGroup>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh Style="text-align: left;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Name</MudTd>
<MudTd Style="text-align: left;">
<MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" Class="ma-2" OnClick="() => this.EditRetrievalProcess(context)">
Edit
</MudButton>
<MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" Class="ma-2" OnClick="() => this.DeleteRetrievalProcess(context)">
Delete
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
@if (this.retrievalProcesses.Count == 0)
{
<MudText Typo="Typo.h6" Class="mt-3">No retrieval process configured yet.</MudText>
}
}
<MudButton Disabled="@this.IsNoneERIServerSelected" Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddRetrievalProcess">
Add Retrieval Process
</MudButton>
<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
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
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.
</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"/>
<MudText Typo="Typo.h4" Class="mt-9 mb-1">
Provider selection for generation
</MudText>
<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.
Small local models will probably not be sufficient. Instead, try using a large cloud-based or a large self-hosted model.
</MudJustifiedText>
<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
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
and can generate responses faster. AI Studio dynamically adapts its behavior and always tries to achieve the fastest possible data processing.
</MudJustifiedText>
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudText Typo="Typo.h4" Class="mt-9 mb-1">
Write code to file system
</MudText>
<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
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.
</MudJustifiedText>
<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
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,
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.
</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" />
<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" />
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,9 @@
namespace AIStudio.Assistants.ERI;
public enum Auth
{
NONE,
KERBEROS,
USERNAME_PASSWORD,
TOKEN,
}
@@ -0,0 +1,26 @@
namespace AIStudio.Assistants.ERI;
public static class AuthExtensions
{
public static string Name(this Auth auth) => auth switch
{
Auth.NONE => "No login necessary: useful for public data sources",
Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos: very complex to implement and to operate, useful for many users",
Auth.USERNAME_PASSWORD => "Login by username and password: simple to implement and to operate, useful for few users; easy to use for users",
Auth.TOKEN => "Login by token: simple to implement and to operate, useful for few users; unusual for many users",
_ => "Unknown login method"
};
public static string ToPrompt(this Auth auth) => auth switch
{
Auth.NONE => "No login is necessary, the data source is public.",
Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos.",
Auth.USERNAME_PASSWORD => "Login by username and password.",
Auth.TOKEN => "Login by static token per user.",
_ => string.Empty,
};
}
@@ -0,0 +1,15 @@
namespace AIStudio.Assistants.ERI;
public enum DataSources
{
NONE,
CUSTOM,
FILE_SYSTEM,
OBJECT_STORAGE,
KEY_VALUE_STORE,
DOCUMENT_STORE,
RELATIONAL_DATABASE,
GRAPH_DATABASE,
}
@@ -0,0 +1,19 @@
namespace AIStudio.Assistants.ERI;
public static class DataSourcesExtensions
{
public static string Name(this DataSources dataSource) => dataSource switch
{
DataSources.NONE => "No data source selected",
DataSources.CUSTOM => "Custom description",
DataSources.FILE_SYSTEM => "File system (local or network share)",
DataSources.OBJECT_STORAGE => "Object storage, like Amazon S3, MinIO, etc.",
DataSources.KEY_VALUE_STORE => "Key-Value store, like Redis, etc.",
DataSources.DOCUMENT_STORE => "Document store, like MongoDB, etc.",
DataSources.RELATIONAL_DATABASE => "Relational database, like MySQL, PostgreSQL, etc.",
DataSources.GRAPH_DATABASE => "Graph database, like Neo4j, ArangoDB, etc.",
_ => "Unknown data source"
};
}
@@ -0,0 +1,8 @@
namespace AIStudio.Assistants.ERI;
public enum ERIVersion
{
NONE,
V1,
}
@@ -0,0 +1,27 @@
namespace AIStudio.Assistants.ERI;
public static class ERIVersionExtensions
{
public static async Task<string> ReadSpecification(this ERIVersion version, HttpClient httpClient)
{
try
{
var url = version.SpecificationURL();
using var response = await httpClient.GetAsync(url);
return await response.Content.ReadAsStringAsync();
}
catch
{
return string.Empty;
}
}
public static string SpecificationURL(this ERIVersion version)
{
var nameLower = version.ToString().ToLowerInvariant();
var filename = $"{nameLower}.json";
return $"specs/eri/{filename}";
}
public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE;
}
@@ -0,0 +1,19 @@
namespace AIStudio.Assistants.ERI;
/// <summary>
/// Represents information about the used embedding for a data source.
/// </summary>
/// <param name="EmbeddingType">What kind of embedding is used. For example, "Transformer Embedding," "Contextual Word
/// Embedding," "Graph Embedding," etc.</param>
/// <param name="EmbeddingName">Name the embedding used. This can be a library, a framework, or the name of the used
/// algorithm.</param>
/// <param name="Description">A short description of the embedding. Describe what the embedding is doing.</param>
/// <param name="UsedWhen">Describe when the embedding is used. For example, when the user prompt contains certain
/// keywords, or anytime?</param>
/// <param name="Link">A link to the embedding's documentation or the source code. Might be null.</param>
public readonly record struct EmbeddingInfo(
string EmbeddingType,
string EmbeddingName,
string Description,
string UsedWhen,
string? Link);
@@ -0,0 +1,9 @@
namespace AIStudio.Assistants.ERI;
public enum OperatingSystem
{
NONE,
WINDOWS,
LINUX,
}
@@ -0,0 +1,14 @@
namespace AIStudio.Assistants.ERI;
public static class OperatingSystemExtensions
{
public static string Name(this OperatingSystem os) => os switch
{
OperatingSystem.NONE => "No operating system specified",
OperatingSystem.WINDOWS => "Windows",
OperatingSystem.LINUX => "Linux",
_ => "Unknown operating system"
};
}
@@ -0,0 +1,20 @@
namespace AIStudio.Assistants.ERI;
public enum ProgrammingLanguages
{
NONE,
C,
CPP,
CSHARP,
GO,
JAVA,
JAVASCRIPT,
JULIA,
MATLAB,
PHP,
PYTHON,
RUST,
OTHER,
}
@@ -0,0 +1,24 @@
namespace AIStudio.Assistants.ERI;
public static class ProgrammingLanguagesExtensions
{
public static string Name(this ProgrammingLanguages language) => language switch
{
ProgrammingLanguages.NONE => "No programming language selected",
ProgrammingLanguages.C => "C",
ProgrammingLanguages.CPP => "C++",
ProgrammingLanguages.CSHARP => "C#",
ProgrammingLanguages.GO => "Go",
ProgrammingLanguages.JAVA => "Java",
ProgrammingLanguages.JAVASCRIPT => "JavaScript",
ProgrammingLanguages.JULIA => "Julia",
ProgrammingLanguages.MATLAB => "MATLAB",
ProgrammingLanguages.PHP => "PHP",
ProgrammingLanguages.PYTHON => "Python",
ProgrammingLanguages.RUST => "Rust",
ProgrammingLanguages.OTHER => "Other",
_ => "Unknown"
};
}
@@ -0,0 +1,18 @@
namespace AIStudio.Assistants.ERI;
/// <summary>
/// Information about a retrieval process, which this data source implements.
/// </summary>
/// <param name="Name">The name of the retrieval process, e.g., "Keyword-Based Wikipedia Article Retrieval".</param>
/// <param name="Description">A short description of the retrieval process. What kind of retrieval process is it?</param>
/// <param name="Link">A link to the retrieval process's documentation, paper, Wikipedia article, or the source code. Might be null.</param>
/// <param name="ParametersDescription">A dictionary that describes the parameters of the retrieval process. The key is the parameter name,
/// and the value is a description of the parameter. Although each parameter will be sent as a string, the description should indicate the
/// expected type and range, e.g., 0.0 to 1.0 for a float parameter.</param>
/// <param name="Embeddings">A list of embeddings used in this retrieval process. It might be empty in case no embedding is used.</param>
public readonly record struct RetrievalInfo(
string Name,
string Description,
string? Link,
Dictionary<string, string>? ParametersDescription,
List<EmbeddingInfo>? Embeddings);
@@ -0,0 +1,14 @@
namespace AIStudio.Assistants.ERI;
public sealed class RetrievalParameter
{
/// <summary>
/// The name of the parameter.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The description of the parameter.
/// </summary>
public string Description { get; set; } = string.Empty;
}
@@ -1,5 +1,5 @@
@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"/>
<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" />
@@ -1,8 +1,9 @@
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
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;
@@ -42,13 +43,13 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore
protected override string SubmitText => "Proofread";
protected override Func<Task> SubmitAction => this.ProofreadText;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
this.correctedText = string.Empty;
@@ -1,5 +1,5 @@
@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"/>
@@ -1,6 +1,8 @@
using AIStudio.Dialogs.Settings;
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;
@@ -32,8 +34,8 @@ public partial class AssistantIconFinder : AssistantBaseCore
protected override string SubmitText => "Find Icon";
protected override Func<Task> SubmitAction => this.FindIcon;
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputContext = string.Empty;
if (!this.MightPreselectValues())
@@ -1,5 +1,5 @@
@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.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."/>
@@ -1,8 +1,9 @@
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
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;
@@ -59,7 +60,7 @@ public partial class AssistantJobPostings : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputEntryDate = string.Empty;
this.inputValidUntil = string.Empty;
@@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_LEGAL_CHECK)]
@inherits AssistantBaseCore
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogLegalCheck>
@if (!this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)
{
@@ -1,8 +1,9 @@
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
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;
@@ -29,7 +30,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore
protected override string SubmitText => "Ask your questions";
protected override Func<Task> SubmitAction => this.AksQuestions;
protected override bool SubmitDisabled => this.isAgentRunning;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
@@ -37,7 +38,7 @@ public partial class AssistantLegalCheck : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputLegalDocument = string.Empty;
this.inputQuestions = string.Empty;
@@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_MY_TASKS)]
@inherits AssistantBaseCore
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogMyTasks>
<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"/>
@@ -1,9 +1,10 @@
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
using AIStudio.Settings;
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;
@@ -41,7 +42,7 @@ public partial class AssistantMyTasks : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
if (!this.MightPreselectValues())
@@ -1,5 +1,5 @@
@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"/>
<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" />
@@ -1,8 +1,9 @@
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.RewriteImprove;
public partial class AssistantRewriteImprove : AssistantBaseCore
public partial class AssistantRewriteImprove : AssistantBaseCore<SettingsDialogRewrite>
{
public override Tools.Components Component => Tools.Components.REWRITE_ASSISTANT;
@@ -43,13 +44,13 @@ public partial class AssistantRewriteImprove : AssistantBaseCore
protected override string SubmitText => "Improve";
protected override Func<Task> SubmitAction => this.RewriteText;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
this.rewrittenText = string.Empty;
@@ -1,5 +1,5 @@
@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.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"/>
@@ -1,8 +1,9 @@
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Synonym;
public partial class AssistantSynonyms : AssistantBaseCore
public partial class AssistantSynonyms : AssistantBaseCore<SettingsDialogSynonyms>
{
public override Tools.Components Component => Tools.Components.SYNONYMS_ASSISTANT;
@@ -54,13 +55,13 @@ public partial class AssistantSynonyms : AssistantBaseCore
protected override string SubmitText => "Find synonyms";
protected override Func<Task> SubmitAction => this.FindSynonyms;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
{
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
this.inputContext = string.Empty;
@@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_SUMMARIZER)]
@inherits AssistantBaseCore
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogTextSummarizer>
@if (!this.SettingsManager.ConfigurationData.TextSummarizer.HideWebContentReader)
{
@@ -1,8 +1,9 @@
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
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;
@@ -32,7 +33,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
protected override string SubmitText => "Summarize";
protected override Func<Task> SubmitAction => this.SummarizeText;
protected override bool SubmitDisabled => this.isAgentRunning;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
@@ -40,7 +41,7 @@ public partial class AssistantTextSummarizer : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
if(!this.MightPreselectValues())
@@ -1,5 +1,5 @@
@attribute [Route(Routes.ASSISTANT_TRANSLATION)]
@inherits AssistantBaseCore
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogTranslation>
@if (!this.SettingsManager.ConfigurationData.Translation.HideWebContentReader)
{
@@ -1,8 +1,9 @@
using AIStudio.Chat;
using AIStudio.Dialogs.Settings;
namespace AIStudio.Assistants.Translation;
public partial class AssistantTranslation : AssistantBaseCore
public partial class AssistantTranslation : AssistantBaseCore<SettingsDialogTranslation>
{
public override Tools.Components Component => Tools.Components.TRANSLATION_ASSISTANT;
@@ -28,7 +29,7 @@ public partial class AssistantTranslation : AssistantBaseCore
protected override string SubmitText => "Translate";
protected override Func<Task> SubmitAction => () => this.TranslateText(true);
protected override bool SubmitDisabled => this.isAgentRunning;
protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with
@@ -36,7 +37,7 @@ public partial class AssistantTranslation : AssistantBaseCore
SystemPrompt = SystemPrompts.DEFAULT,
};
protected override void ResetFrom()
protected override void ResetForm()
{
this.inputText = string.Empty;
this.inputTextLastTranslation = string.Empty;
+186
View File
@@ -1,3 +1,8 @@
using AIStudio.Components;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient.DataModel;
namespace AIStudio.Chat;
/// <summary>
@@ -15,6 +20,36 @@ public sealed record ChatThread
/// </summary>
public Guid WorkspaceId { get; set; }
/// <summary>
/// Specifies the provider selected for the chat thread.
/// </summary>
public string SelectedProvider { get; set; } = string.Empty;
/// <summary>
/// Specifies the profile selected for the chat thread.
/// </summary>
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>
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
/// </summary>
@@ -34,4 +69,155 @@ public sealed record ChatThread
/// The content blocks of the chat thread.
/// </summary>
public List<ContentBlock> Blocks { get; init; } = [];
/// <summary>
/// Prepares the system prompt for the chat thread.
/// </summary>
/// <remarks>
/// The actual system prompt depends on the selected profile. If no profile is selected,
/// the system prompt is returned as is. When a profile is selected, the system prompt
/// is extended with the profile chosen.
/// </remarks>
/// <param name="settingsManager">The settings manager instance to use.</param>
/// <param name="chatThread">The chat thread to prepare the system prompt for.</param>
/// <param name="logger">The logger instance to use.</param>
/// <returns>The prepared system prompt.</returns>
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:
//
string systemPromptText;
var logMessage = $"Using no profile for chat thread '{chatThread.Name}'.";
if (string.IsNullOrWhiteSpace(chatThread.SelectedProfile))
systemPromptText = systemPromptWithAugmentedData;
else
{
if(!Guid.TryParse(chatThread.SelectedProfile, out var profileId))
systemPromptText = systemPromptWithAugmentedData;
else
{
if(chatThread.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
systemPromptText = systemPromptWithAugmentedData;
else
{
var profile = settingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatThread.SelectedProfile);
if(profile == default)
systemPromptText = systemPromptWithAugmentedData;
else
{
logMessage = $"Using profile '{profile.Name}' for chat thread '{chatThread.Name}'.";
systemPromptText = $"""
{systemPromptWithAugmentedData}
{profile.ToSystemPrompt()}
""";
}
}
}
}
logger.LogInformation(logMessage);
return systemPromptText;
}
/// <summary>
/// Removes a content block from this chat thread.
/// </summary>
/// <param name="content">The content block to remove.</param>
/// <param name="removeForRegenerate">Indicates whether the content block is removed for
/// regeneration purposes. True, when the content block is removed for regeneration purposes,
/// which will not remove the previous user block if it is hidden from the user.</param>
public void Remove(IContent content, bool removeForRegenerate = false)
{
var block = this.Blocks.FirstOrDefault(x => x.Content == content);
if(block is null)
return;
//
// Remove the previous user block if it is hidden from the user. Otherwise,
// the experience might be confusing for the user.
//
// Explanation, using the ERI assistant as an example:
// - The ERI assistant generates for every file a hidden user prompt.
// - In the UI, the user can only see the AI's responses, not the hidden user prompts.
// - Now, the user removes one AI response
// - The hidden user prompt is still there, but the user can't see it.
// - Since the user prompt is hidden, neither is it possible to remove nor edit it.
// - This method solves this issue by removing the hidden user prompt when the AI response is removed.
//
if (block.Role is ChatRole.AI && !removeForRegenerate)
{
var sortedBlocks = this.Blocks.OrderBy(x => x.Time).ToList();
var index = sortedBlocks.IndexOf(block);
if (index > 0)
{
var previousBlock = sortedBlocks[index - 1];
if (previousBlock.Role is ChatRole.USER && previousBlock.HideFromUser)
this.Blocks.Remove(previousBlock);
}
}
// Remove the block from the chat thread:
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.UNKNOW,
},
Content = contentData,
Type = contentType,
});
}
return new Tools.ERIClient.DataModel.ChatThread { ContentBlocks = contentBlocks };
}
}
@@ -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,
};
}
}
+6 -1
View File
@@ -18,10 +18,15 @@ public class ContentBlock
/// <summary>
/// The content of the block.
/// </summary>
public IContent? Content { get; init; } = null;
public IContent? Content { get; init; }
/// <summary>
/// The role of the content block in the chat thread, e.g., user, AI, etc.
/// </summary>
public ChatRole Role { get; init; } = ChatRole.NONE;
/// <summary>
/// Should the content block be hidden from the user?
/// </summary>
public bool HideFromUser { get; set; }
}
@@ -12,7 +12,33 @@
<MudText Typo="Typo.body1">@this.Role.ToName() (@this.Time)</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard" />
@if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null)
{
<MudTooltip Text="Edit" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastUserBlock"/>
</MudTooltip>
}
@if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null)
{
<MudTooltip Text="Edit" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastBlock"/>
</MudTooltip>
}
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
{
<MudTooltip Text="Regenerate" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
</MudTooltip>
}
@if (this.RemoveBlockFunc is not null)
{
<MudTooltip Text="Removes this block" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/>
</MudTooltip>
}
<MudTooltip Text="Copies the content to the clipboard" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy" Color="Color.Default" OnClick="@this.CopyToClipboard"/>
</MudTooltip>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
@@ -44,7 +70,7 @@
}
else
{
<MudMarkdown Value="@textContent.Text" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo"/>
<MudMarkdown Value="@textContent.Text" OverrideHeaderTypo="@Markdown.OverrideHeaderTypo" CodeBlockTheme="@this.CodeColorPalette"/>
}
}
}
@@ -52,9 +78,9 @@
break;
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;
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Components;
using AIStudio.Settings;
using AIStudio.Tools.Services;
using RustService = AIStudio.Tools.RustService;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Chat;
@@ -39,11 +40,38 @@ public partial class ContentBlockComponent : ComponentBase
[Parameter]
public string Class { get; set; } = string.Empty;
[Parameter]
public bool IsLastContentBlock { get; set; } = false;
[Parameter]
public bool IsSecondToLastBlock { get; set; } = false;
[Parameter]
public Func<IContent, Task>? RemoveBlockFunc { get; set; }
[Parameter]
public Func<IContent, Task>? RegenerateFunc { get; set; }
[Parameter]
public Func<IContent, Task>? EditLastBlockFunc { get; set; }
[Parameter]
public Func<IContent, Task>? EditLastUserBlockFunc { get; set; }
[Parameter]
public Func<bool> RegenerateEnabled { get; set; } = () => false;
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private ISnackbar Snackbar { get; init; } = null!;
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
private bool HideContent { get; set; }
@@ -64,7 +92,7 @@ public partial class ContentBlockComponent : ComponentBase
private async Task AfterStreaming()
{
// Might be called from a different thread, so we need to invoke the UI thread:
await this.InvokeAsync(() =>
await this.InvokeAsync(async () =>
{
//
// Issue we try to solve: When the content changes during streaming,
@@ -83,6 +111,9 @@ public partial class ContentBlockComponent : ComponentBase
// Let Blazor update the UI, i.e., to see the render tree diff:
this.StateHasChanged();
// Inform the chat that the streaming is done:
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.CHAT_STREAMING_DONE);
});
}
@@ -112,4 +143,68 @@ public partial class ContentBlockComponent : ComponentBase
}
private string CardClasses => $"my-2 rounded-lg {this.Class}";
private CodeBlockTheme CodeColorPalette => this.SettingsManager.IsDarkMode ? CodeBlockTheme.Dark : CodeBlockTheme.Default;
private async Task RemoveBlock()
{
if (this.RemoveBlockFunc is null)
return;
var remove = await this.DialogService.ShowMessageBox(
"Remove Message",
"Do you really want to remove this message?",
"Yes, remove it",
"No, keep it");
if (remove.HasValue && remove.Value)
await this.RemoveBlockFunc(this.Content);
}
private async Task RegenerateBlock()
{
if (this.RegenerateFunc is null)
return;
if(this.Role is not ChatRole.AI)
return;
var regenerate = await this.DialogService.ShowMessageBox(
"Regenerate Message",
"Do you really want to regenerate this message?",
"Yes, regenerate it",
"No, keep it");
if (regenerate.HasValue && regenerate.Value)
await this.RegenerateFunc(this.Content);
}
private async Task EditLastBlock()
{
if (this.EditLastBlockFunc is null)
return;
if(this.Role is not ChatRole.USER)
return;
await this.EditLastBlockFunc(this.Content);
}
private async Task EditLastUserBlock()
{
if (this.EditLastUserBlockFunc is null)
return;
if(this.Role is not ChatRole.USER)
return;
var edit = await this.DialogService.ShowMessageBox(
"Edit Message",
"Do you really want to edit this message? In order to edit this message, the AI response will be deleted.",
"Yes, remove the AI response and edit it",
"No, keep it");
if (edit.HasValue && edit.Value)
await this.EditLastUserBlockFunc(this.Content);
}
}
+9 -7
View File
@@ -1,14 +1,13 @@
using System.Text.Json.Serialization;
using AIStudio.Provider;
using AIStudio.Settings;
namespace AIStudio.Chat;
/// <summary>
/// Represents an image inside the chat.
/// </summary>
public sealed class ContentImage : IContent
public sealed class ContentImage : IContent, IImageSource
{
#region Implementation of IContent
@@ -29,7 +28,7 @@ public sealed class ContentImage : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <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();
}
@@ -37,12 +36,15 @@ public sealed class ContentImage : IContent
#endregion
/// <summary>
/// The URL of the image.
/// The type of the image source.
/// </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>
/// The local path of the image.
/// The image source.
/// </summary>
public string LocalPath { get; set; } = string.Empty;
public required string Source { get; set; }
}
@@ -0,0 +1,8 @@
namespace AIStudio.Chat;
public enum ContentImageSource
{
URL,
LOCAL_PATH,
BASE64,
}
+31 -4
View File
@@ -2,6 +2,7 @@ using System.Text.Json.Serialization;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.RAG.RAGProcesses;
namespace AIStudio.Chat;
@@ -35,17 +36,42 @@ public sealed class ContentText : IContent
public Func<Task> StreamingEvent { get; set; } = () => Task.CompletedTask;
/// <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)
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;
}
// Store the last time we got a response. We use this ater
// 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
// to determine whether we should notify the UI about the
// new content or not. Depends on the energy saving mode
// the user chose.
var last = DateTimeOffset.Now;
// Get the settings manager:
var settings = Program.SERVICE_PROVIDER.GetService<SettingsManager>()!;
// Start another thread by using a task to uncouple
// the UI thread from the AI processing:
await Task.Run(async () =>
@@ -54,7 +80,7 @@ public sealed class ContentText : IContent
this.InitialRemoteWait = true;
// Iterate over the responses from the AI:
await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, token))
await foreach (var deltaText in provider.StreamChatCompletion(chatModel, chatThread, settings, token))
{
// When the user cancels the request, we stop the loop:
if (token.IsCancellationRequested)
@@ -96,6 +122,7 @@ public sealed class ContentText : IContent
// Inform the UI that the streaming is done:
await this.StreamingDone();
return chatThread;
}
#endregion
+12 -2
View File
@@ -1,7 +1,6 @@
using System.Text.Json.Serialization;
using AIStudio.Provider;
using AIStudio.Settings;
namespace AIStudio.Chat;
@@ -42,5 +41,16 @@ public interface IContent
/// <summary>
/// Uses the provider to create the content.
/// </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,
};
}
@@ -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; }
}
@@ -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;
}
}
}
@@ -0,0 +1,7 @@
namespace AIStudio.Chat;
public static class KnownWorkspaces
{
public static readonly Guid BIAS_WORKSPACE_ID = Guid.Parse("82050a4e-ee92-43d7-8ee5-ab512f847e02");
public static readonly Guid ERI_SERVER_WORKSPACE_ID = Guid.Parse("8ec09cd3-9da7-4736-b245-2d8b67fc342f");
}
@@ -1,3 +1,5 @@
@typeparam TSettings
<MudCard Outlined="@true" Style="@this.BlockStyle">
<MudCardHeader>
<CardHeaderContent>
@@ -17,8 +19,11 @@
</MudStack>
</MudCardContent>
<MudCardActions>
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link">
@this.ButtonText
</MudButton>
<MudButtonGroup Variant="Variant.Outlined">
<MudButton Size="Size.Large" Variant="Variant.Filled" StartIcon="@this.Icon" Color="Color.Default" Href="@this.Link">
@this.ButtonText
</MudButton>
<MudIconButton Variant="Variant.Text" Icon="@Icons.Material.Filled.Settings" Color="Color.Default" OnClick="@this.OpenSettingsDialog"/>
</MudButtonGroup>
</MudCardActions>
</MudCard>
@@ -2,9 +2,11 @@ using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components;
public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDisposable
public partial class AssistantBlock<TSettings> : ComponentBase, IMessageBusReceiver, IDisposable where TSettings : IComponent
{
[Parameter]
public string Name { get; set; } = string.Empty;
@@ -29,6 +31,16 @@ public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDispo
[Inject]
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
@@ -44,6 +56,8 @@ public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDispo
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(AssistantBlock<TSettings>);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
@@ -62,7 +76,7 @@ public partial class AssistantBlock : ComponentBase, IMessageBusReceiver, IDispo
}
#endregion
private string BorderColor => this.SettingsManager.IsDarkMode switch
{
true => this.ColorTheme.GetCurrentPalette(this.SettingsManager).GrayLight,
@@ -13,6 +13,29 @@ public partial class Changelog
public static readonly Log[] LOGS =
[
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 (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 (197, "v0.9.22, build 197 (2024-12-04 10:58 UTC)", "v0.9.22.md"),
new (196, "v0.9.21, build 196 (2024-11-23 12:22 UTC)", "v0.9.21.md"),
new (195, "v0.9.20, build 195 (2024-11-16 20:44 UTC)", "v0.9.20.md"),
new (194, "v0.9.19, build 194 (2024-11-14 05:58 UTC)", "v0.9.19.md"),
new (193, "v0.9.18, build 193 (2024-11-09 21:10 UTC)", "v0.9.18.md"),
new (192, "v0.9.17, build 192 (2024-11-03 11:11 UTC)", "v0.9.17.md"),
new (191, "v0.9.16, build 191 (2024-11-02 22:04 UTC)", "v0.9.16.md"),
new (190, "v0.9.15, build 190 (2024-10-28 15:04 UTC)", "v0.9.15.md"),
new (189, "v0.9.14, build 189 (2024-10-18 08:48 UTC)", "v0.9.14.md"),
new (188, "v0.9.13, build 188 (2024-10-07 11:18 UTC)", "v0.9.13.md"),
new (187, "v0.9.12, build 187 (2024-09-15 20:49 UTC)", "v0.9.12.md"),
new (186, "v0.9.11, build 186 (2024-09-15 10:33 UTC)", "v0.9.11.md"),
@@ -23,7 +23,7 @@ public partial class Changelog : ComponentBase
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();
}
}
@@ -0,0 +1,126 @@
@using AIStudio.Settings.DataModel
@using AIStudio.Chat
@inherits MSGComponentBase
<InnerScrolling FillEntireHorizontalSpace="@true" @ref="@this.scrollingArea" MinWidth="36em" Style="height: 100%">
<ChildContent>
@if (this.ChatThread is not null)
{
var blocks = this.ChatThread.Blocks.OrderBy(n => n.Time).ToList();
for (var i = 0; i < blocks.Count; i++)
{
var block = blocks[i];
var isLastBlock = i == blocks.Count - 1;
var isSecondLastBlock = i == blocks.Count - 2;
@if (!block.HideFromUser)
{
<ContentBlockComponent
Role="@block.Role"
Type="@block.ContentType"
Time="@block.Time"
Content="@block.Content"
RemoveBlockFunc="@this.RemoveBlock"
IsLastContentBlock="@isLastBlock"
IsSecondToLastBlock="@isSecondLastBlock"
RegenerateFunc="@this.RegenerateBlock"
RegenerateEnabled="@(() => this.IsProviderSelected && this.ChatThread.IsLLMProviderAllowed(this.Provider))"
EditLastBlockFunc="@this.EditLastBlock"
EditLastUserBlockFunc="@this.EditLastUserBlock"/>
}
}
}
</ChildContent>
<FooterContent>
<MudElement Style="flex: 0 0 auto;">
<MudTextField
T="string"
@ref="@this.inputField"
@bind-Text="@this.userInput"
Variant="Variant.Outlined"
AutoGrow="@true"
Lines="3"
MaxLines="12"
Label="@this.InputLabel"
Placeholder="@this.ProviderPlaceholder"
Adornment="Adornment.End"
AdornmentIcon="@Icons.Material.Filled.Send"
OnAdornmentClick="() => this.SendMessage()"
Disabled="@this.IsInputForbidden()"
Immediate="@true"
OnKeyUp="this.InputKeyEvent"
UserAttributes="@USER_INPUT_ATTRIBUTES"
Class="@this.UserInputClass"
Style="@this.UserInputStyle"/>
</MudElement>
<MudToolBar WrapContent="true" Gutters="@false" Class="border border-solid rounded" Style="border-color: lightgrey;">
@if (
this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES
&& this.SettingsManager.ConfigurationData.Workspace.DisplayBehavior is WorkspaceDisplayBehavior.TOGGLE_OVERLAY)
{
<MudTooltip Text="Show your workspaces" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.SnippetFolder" OnClick="() => this.ToggleWorkspaceOverlay()"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY)
{
<MudTooltip Text="Save chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="() => this.SaveThread()" Disabled="@(!this.CanThreadBeSaved)"/>
</MudTooltip>
}
<MudTooltip Text="Start temporary chat" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.AddComment" OnClick="() => this.StartNewChat(useSameWorkspace: false)"/>
</MudTooltip>
@if (!string.IsNullOrWhiteSpace(this.currentWorkspaceName))
{
<MudTooltip Text="@this.TooltipAddChatToWorkspace" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.CommentBank" OnClick="() => this.StartNewChat(useSameWorkspace: true)"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
<MudTooltip Text="Delete this chat & start a new one" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="() => this.StartNewChat(useSameWorkspace: true, deletePreviousChat: true)" Disabled="@(!this.CanThreadBeSaved)"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is not WorkspaceStorageBehavior.DISABLE_WORKSPACES)
{
<MudTooltip Text="Move the chat to a workspace, or to another if it is already in one." Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.MoveToInbox" Disabled="@(!this.CanThreadBeSaved)" OnClick="() => this.MoveChatToWorkspace()"/>
</MudTooltip>
}
@if (this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence)
{
<ConfidenceInfo Mode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider.UsedLLMProvider"/>
}
@if (this.isStreaming && this.cancellationTokenSource is not null)
{
<MudTooltip Text="Stop generation" Placement="@TOOLBAR_TOOLTIP_PLACEMENT">
<MudIconButton Icon="@Icons.Material.Filled.Stop" Color="Color.Error" OnClick="() => this.CancelStreaming()"/>
</MudTooltip>
}
<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>
</FooterContent>
</InnerScrolling>
@@ -0,0 +1,904 @@
using AIStudio.Chat;
using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components;
public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
{
[Parameter]
public ChatThread? ChatThread { get; set; }
[Parameter]
public EventCallback<ChatThread?> ChatThreadChanged { get; set; }
[Parameter]
public AIStudio.Settings.Provider Provider { get; set; }
[Parameter]
public EventCallback<AIStudio.Settings.Provider> ProviderChanged { get; set; }
[Parameter]
public Action<string> WorkspaceName { get; set; } = _ => { };
[Parameter]
public Workspaces? Workspaces { get; set; }
[Inject]
private ILogger<ChatComponent> Logger { get; set; } = null!;
[Inject]
private ThreadSafeRandom RNG { get; init; } = null!;
[Inject]
private IDialogService DialogService { get; init; } = null!;
[Inject]
private DataSourceService DataSourceService { get; init; } = null!;
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private DataSourceSelection? dataSourceSelectionComponent;
private DataSourceOptions earlyDataSourceOptions = new();
private Profile currentProfile = Profile.NO_PROFILE;
private bool hasUnsavedChanges;
private bool mustScrollToBottomAfterRender;
private InnerScrolling scrollingArea = null!;
private byte scrollRenderCountdown;
private bool isStreaming;
private string userInput = string.Empty;
private bool mustStoreChat;
private bool mustLoadChat;
private LoadChat loadChat;
private bool autoSaveEnabled;
private string currentWorkspaceName = string.Empty;
private Guid currentWorkspaceId = Guid.Empty;
private CancellationTokenSource? cancellationTokenSource;
// Unfortunately, we need the input field reference to blur the focus away. Without
// this, we cannot clear the input field.
private MudTextField<string> inputField = null!;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Apply the filters for the message bus:
this.ApplyFilters([], [ Event.HAS_CHAT_UNSAVED_CHANGES, Event.RESET_CHAT_STATE, Event.CHAT_STREAMING_DONE, Event.WORKSPACE_LOADED_CHAT_CHANGED ]);
// Configure the spellchecking for the user input:
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
// Get the preselected profile:
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();
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.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);
// 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)
{
//
// Check if the chat thread has a name. If not, we
// generate the name now:
//
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
{
var firstUserBlock = this.ChatThread.Blocks.FirstOrDefault(x => x.Role == ChatRole.USER);
if (firstUserBlock is not null)
{
this.ChatThread.Name = firstUserBlock.Content switch
{
ContentText textBlock => this.ExtractThreadName(textBlock.Text),
_ => "Thread"
};
}
}
//
// 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)
{
this.autoSaveEnabled = true;
this.mustStoreChat = true;
//
// When a standard workspace is used, we have to ensure
// that the workspace is available:
//
if(this.ChatThread.WorkspaceId == KnownWorkspaces.ERI_SERVER_WORKSPACE_ID)
await WorkspaceBehaviour.EnsureERIServerWorkspace();
else if (this.ChatThread.WorkspaceId == KnownWorkspaces.BIAS_WORKSPACE_ID)
await WorkspaceBehaviour.EnsureBiasWorkspace();
}
}
}
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)
{
//
// 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.scrollRenderCountdown = 4;
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();
if (deferredLoading != default)
{
this.loadChat = deferredLoading;
this.mustLoadChat = true;
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 base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && this.ChatThread is not null && this.mustStoreChat)
{
this.mustStoreChat = false;
if(this.Workspaces is not null)
await this.Workspaces.StoreChat(this.ChatThread);
else
await WorkspaceBehaviour.StoreChat(this.ChatThread);
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
}
if (firstRender && this.mustLoadChat)
{
this.Logger.LogInformation($"Try to load the chat '{this.loadChat.ChatId}' now.");
this.mustLoadChat = false;
this.ChatThread = await WorkspaceBehaviour.LoadChat(this.loadChat);
if(this.ChatThread is not null)
{
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
this.Logger.LogInformation($"The chat '{this.ChatThread!.ChatId}' with title '{this.ChatThread.Name}' ({this.ChatThread.Blocks.Count} messages) was loaded successfully.");
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
await this.SelectProviderWhenLoadingChat();
}
else
this.Logger.LogWarning($"The chat '{this.loadChat.ChatId}' could not be loaded.");
this.StateHasChanged();
}
if(this.mustScrollToBottomAfterRender)
{
if (--this.scrollRenderCountdown == 0)
{
await this.scrollingArea.ScrollToBottom();
this.mustScrollToBottomAfterRender = false;
}
else
{
this.StateHasChanged();
}
}
await base.OnAfterRenderAsync(firstRender);
}
#endregion
private bool IsProviderSelected => this.Provider.UsedLLMProvider != LLMProviders.NONE;
private string ProviderPlaceholder => this.IsProviderSelected ? "Type your input here..." : "Select a provider first";
private string InputLabel => this.IsProviderSelected ? $"Your Prompt (use selected instance '{this.Provider.InstanceName}', provider '{this.Provider.UsedLLMProvider.ToName()}')" : "Select a provider first";
private bool CanThreadBeSaved => this.ChatThread is not null && this.ChatThread.Blocks.Count > 0;
private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\"";
private string UserInputStyle => this.SettingsManager.ConfigurationData.LLMProviders.ShowProviderConfidence ? this.Provider.UsedLLMProvider.GetConfidence(this.SettingsManager).SetColorStyle(this.SettingsManager) : 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)
{
// We select the first 10 words of the user input:
var words = firstUserInput.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var threadName = string.Join(' ', words.Take(10));
// If the thread name is empty, we use a default name:
if (string.IsNullOrWhiteSpace(threadName))
threadName = "Thread";
return threadName;
}
private async Task ProfileWasChanged(Profile profile)
{
this.currentProfile = profile;
if(this.ChatThread is null)
return;
this.ChatThread = this.ChatThread with
{
SelectedProfile = this.currentProfile.Id,
};
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)
{
if(this.dataSourceSelectionComponent?.IsVisible ?? false)
this.dataSourceSelectionComponent.Hide();
this.hasUnsavedChanges = true;
var key = keyEvent.Code.ToLowerInvariant();
// Was the enter key (either enter or numpad enter) pressed?
var isEnter = key is "enter" or "numpadenter";
// Was a modifier key pressed as well?
var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey;
// Depending on the user's settings, might react to shortcuts:
switch (this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior)
{
case SendBehavior.ENTER_IS_SENDING:
if (!isModifier && isEnter)
await this.SendMessage();
break;
case SendBehavior.MODIFER_ENTER_IS_SENDING:
if (isEnter && isModifier)
await this.SendMessage();
break;
}
}
private async Task SendMessage(bool reuseLastUserPrompt = false)
{
if (!this.IsProviderSelected)
return;
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
return;
// We need to blur the focus away from the input field
// to be able to clear the field:
await this.inputField.BlurAsync();
// Create a new chat thread if necessary:
if (this.ChatThread is null)
{
this.ChatThread = new()
{
SelectedProvider = this.Provider.Id,
SelectedProfile = this.currentProfile.Id,
SystemPrompt = SystemPrompts.DEFAULT,
WorkspaceId = this.currentWorkspaceId,
ChatId = Guid.NewGuid(),
DataSourceOptions = this.earlyDataSourceOptions,
Name = this.ExtractThreadName(this.userInput),
Seed = this.RNG.Next(),
Blocks = [],
};
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
}
else
{
// Set the thread name if it is empty:
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
this.ChatThread.Name = this.ExtractThreadName(this.userInput);
// Update provider and profile:
this.ChatThread.SelectedProvider = this.Provider.Id;
this.ChatThread.SelectedProfile = this.currentProfile.Id;
}
var time = DateTimeOffset.Now;
IContent? lastUserPrompt;
if (!reuseLastUserPrompt)
{
lastUserPrompt = new ContentText
{
Text = this.userInput,
};
//
// Add the user message to the thread:
//
this.ChatThread?.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = lastUserPrompt,
});
// Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
await this.SaveThread();
this.hasUnsavedChanges = false;
this.StateHasChanged();
}
}
else
lastUserPrompt = this.ChatThread.Blocks.Last(x => x.Role is ChatRole.USER).Content;
//
// Add the AI response to the thread:
//
var aiText = new ContentText
{
// We have to wait for the remote
// for the content stream:
InitialRemoteWait = true,
};
this.ChatThread?.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.AI,
Content = aiText,
});
// Clear the input field:
this.userInput = string.Empty;
// Enable the stream state for the chat component:
this.isStreaming = true;
this.hasUnsavedChanges = true;
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{
this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2;
}
this.Logger.LogDebug($"Start processing user input using provider '{this.Provider.InstanceName}' with model '{this.Provider.Model}'.");
using (this.cancellationTokenSource = new())
{
this.StateHasChanged();
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
this.ChatThread = await aiText.CreateFromProviderAsync(this.Provider.CreateProvider(this.Logger), this.Provider.Model, lastUserPrompt, this.ChatThread, this.cancellationTokenSource.Token);
}
this.cancellationTokenSource = null;
// Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
await this.SaveThread();
this.hasUnsavedChanges = false;
}
// Disable the stream state:
this.isStreaming = false;
// Update the UI:
this.StateHasChanged();
}
private async Task CancelStreaming()
{
if (this.cancellationTokenSource is not null)
if(!this.cancellationTokenSource.IsCancellationRequested)
await this.cancellationTokenSource.CancelAsync();
}
private async Task SaveThread()
{
if(this.ChatThread is null)
return;
if (!this.CanThreadBeSaved)
return;
//
// When the workspace component is visible, we store the chat
// through the workspace component. The advantage of this is that
// the workspace gets updated automatically when the chat is saved.
//
if (this.Workspaces is not null)
await this.Workspaces.StoreChat(this.ChatThread);
else
await WorkspaceBehaviour.StoreChat(this.ChatThread);
this.hasUnsavedChanges = 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)
{
var dialogParameters = new DialogParameters
{
{ "Message", "Are you sure you want to start a new chat? All unsaved changes will be lost." },
};
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Delete Chat", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
}
//
// Delete the previous chat when desired and necessary:
//
if (this.ChatThread is not null && deletePreviousChat)
{
string chatPath;
if (this.ChatThread.WorkspaceId == Guid.Empty)
chatPath = Path.Join(SettingsManager.DataDirectory, "tempChats", this.ChatThread.ChatId.ToString());
else
chatPath = Path.Join(SettingsManager.DataDirectory, "workspaces", this.ChatThread.WorkspaceId.ToString(), this.ChatThread.ChatId.ToString());
if(this.Workspaces is null)
await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
else
await this.Workspaces.DeleteChat(chatPath, askForConfirmation: false, unloadChat: true);
}
//
// Reset our state:
//
this.isStreaming = false;
this.hasUnsavedChanges = false;
this.userInput = string.Empty;
//
// Reset the LLM provider considering the user's settings:
//
switch (this.SettingsManager.ConfigurationData.Chat.AddChatProviderBehavior)
{
case AddChatProviderBehavior.ADDED_CHATS_USE_DEFAULT_PROVIDER:
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
await this.ProviderChanged.InvokeAsync(this.Provider);
break;
default:
case AddChatProviderBehavior.ADDED_CHATS_USE_LATEST_PROVIDER:
if(this.Provider == default)
{
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
await this.ProviderChanged.InvokeAsync(this.Provider);
}
break;
}
//
// Reset the chat thread or create a new one:
//
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.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
}
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()
{
SelectedProvider = this.Provider.Id,
SelectedProfile = this.currentProfile.Id,
SystemPrompt = SystemPrompts.DEFAULT,
WorkspaceId = this.currentWorkspaceId,
ChatId = Guid.NewGuid(),
Name = string.Empty,
Seed = this.RNG.Next(),
Blocks = [],
};
}
// 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);
}
private async Task MoveChatToWorkspace()
{
if(this.ChatThread is null)
return;
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_MANUALLY && this.hasUnsavedChanges)
{
var confirmationDialogParameters = new DialogParameters
{
{ "Message", "Are you sure you want to move this chat? All unsaved changes will be lost." },
};
var confirmationDialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Unsaved Changes", confirmationDialogParameters, DialogOptions.FULLSCREEN);
var confirmationDialogResult = await confirmationDialogReference.Result;
if (confirmationDialogResult is null || confirmationDialogResult.Canceled)
return;
}
var dialogParameters = new DialogParameters
{
{ "Message", "Please select the workspace where you want to move the chat to." },
{ "SelectedWorkspace", this.ChatThread?.WorkspaceId },
{ "ConfirmText", "Move chat" },
};
var dialogReference = await this.DialogService.ShowAsync<WorkspaceSelectionDialog>("Move Chat to Workspace", dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
var workspaceId = dialogResult.Data is Guid id ? id : Guid.Empty;
if (workspaceId == Guid.Empty)
return;
// Delete the chat from the current workspace or the temporary storage:
await WorkspaceBehaviour.DeleteChat(this.DialogService, this.ChatThread!.WorkspaceId, this.ChatThread.ChatId, askForConfirmation: false);
this.ChatThread!.WorkspaceId = workspaceId;
await this.SaveThread();
this.currentWorkspaceId = this.ChatThread.WorkspaceId;
this.currentWorkspaceName = await WorkspaceBehaviour.LoadWorkspaceName(this.ChatThread.WorkspaceId);
this.WorkspaceName(this.currentWorkspaceName);
}
private async Task LoadedChatChanged()
{
this.isStreaming = false;
this.hasUnsavedChanges = false;
this.userInput = string.Empty;
if (this.ChatThread is not null)
{
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();
if (this.SettingsManager.ConfigurationData.Chat.ShowLatestMessageAfterLoading)
{
this.mustScrollToBottomAfterRender = true;
this.scrollRenderCountdown = 2;
}
this.StateHasChanged();
}
private async Task ResetState()
{
this.isStreaming = false;
this.hasUnsavedChanges = false;
this.userInput = string.Empty;
this.currentWorkspaceId = Guid.Empty;
this.currentWorkspaceName = string.Empty;
this.WorkspaceName(this.currentWorkspaceName);
this.ChatThread = null;
this.ApplyStandardDataSourceOptions();
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
}
private async Task SelectProviderWhenLoadingChat()
{
var chatProvider = this.ChatThread?.SelectedProvider;
var chatProfile = this.ChatThread?.SelectedProfile;
switch (this.SettingsManager.ConfigurationData.Chat.LoadingProviderBehavior)
{
default:
case LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE:
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT, chatProvider);
break;
case LoadingChatProviderBehavior.ALWAYS_USE_DEFAULT_CHAT_PROVIDER:
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
break;
case LoadingChatProviderBehavior.ALWAYS_USE_LATEST_CHAT_PROVIDER:
if(this.Provider == default)
this.Provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT);
break;
}
await this.ProviderChanged.InvokeAsync(this.Provider);
// Try to select the profile:
if (!string.IsNullOrWhiteSpace(chatProfile))
{
this.currentProfile = this.SettingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == chatProfile);
if(this.currentProfile == default)
this.currentProfile = Profile.NO_PROFILE;
}
}
private async Task ToggleWorkspaceOverlay()
{
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_TOGGLE_OVERLAY);
}
private async Task RemoveBlock(IContent block)
{
if(this.ChatThread is null)
return;
this.ChatThread.Remove(block);
this.hasUnsavedChanges = true;
await this.SaveThread();
this.StateHasChanged();
}
private async Task RegenerateBlock(IContent aiBlock)
{
if(this.ChatThread is null)
return;
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
return;
this.ChatThread.Remove(aiBlock, removeForRegenerate: true);
this.hasUnsavedChanges = true;
this.StateHasChanged();
await this.SendMessage(reuseLastUserPrompt: true);
}
private Task EditLastUserBlock(IContent block)
{
if(this.ChatThread is null)
return Task.CompletedTask;
if (block is not ContentText textBlock)
return Task.CompletedTask;
var lastBlock = this.ChatThread.Blocks.Last();
var lastBlockContent = lastBlock.Content;
if(lastBlockContent is null)
return Task.CompletedTask;
this.userInput = textBlock.Text;
this.ChatThread.Remove(block);
this.ChatThread.Remove(lastBlockContent);
this.hasUnsavedChanges = true;
this.StateHasChanged();
return Task.CompletedTask;
}
private Task EditLastBlock(IContent block)
{
if(this.ChatThread is null)
return Task.CompletedTask;
if (block is not ContentText textBlock)
return Task.CompletedTask;
this.userInput = textBlock.Text;
this.ChatThread.Remove(block);
this.hasUnsavedChanges = true;
this.StateHasChanged();
return Task.CompletedTask;
}
#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
{
switch (triggeredEvent)
{
case Event.RESET_CHAT_STATE:
await this.ResetState();
break;
case Event.CHAT_STREAMING_DONE:
if(this.autoSaveEnabled)
await this.SaveThread();
break;
case Event.WORKSPACE_LOADED_CHAT_CHANGED:
await this.LoadedChatChanged();
break;
}
}
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
{
switch (triggeredEvent)
{
case Event.HAS_CHAT_UNSAVED_CHANGES:
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
return Task.FromResult((TResult?) (object) false);
return Task.FromResult((TResult?)(object)this.hasUnsavedChanges);
}
return Task.FromResult(default(TResult));
}
#endregion
#region Implementation of IAsyncDisposable
public async ValueTask DisposeAsync()
{
this.MessageBus.Unregister(this);
if(this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
await this.SaveThread();
this.hasUnsavedChanges = false;
}
if (this.cancellationTokenSource is not null)
{
if(!this.cancellationTokenSource.IsCancellationRequested)
await this.cancellationTokenSource.CancelAsync();
this.cancellationTokenSource.Dispose();
}
}
#endregion
}
@@ -1,7 +1,7 @@
@using AIStudio.Provider
<div class="d-flex">
<MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider.">
@if (this.Mode is ConfidenceInfoMode.ICON)
<MudTooltip Text="Shows and hides the confidence card with information about the selected LLM provider." Placement="Placement.Top">
@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())"/>
}
@@ -20,7 +20,7 @@
<MudText Typo="Typo.h5">Confidence Card</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudCardContent Style="max-height: 50vh; max-width: 35vw; overflow: auto;">
<MudText Typo="Typo.h6">Description</MudText>
<MudMarkdown Value="@this.currentConfidence.Description"/>
@@ -8,7 +8,7 @@ namespace AIStudio.Components;
public partial class ConfidenceInfo : ComponentBase, IMessageBusReceiver, IDisposable
{
[Parameter]
public ConfidenceInfoMode Mode { get; set; } = ConfidenceInfoMode.BUTTON;
public PopoverTriggerMode Mode { get; set; } = PopoverTriggerMode.BUTTON;
[Parameter]
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 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
public string ComponentName => nameof(ConfidenceInfo);
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
@@ -57,6 +57,8 @@ public partial class ConfigurationBase : ComponentBase, IMessageBusReceiver, IDi
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfigurationBase);
public Task ProcessMessage<TMsg>(ComponentBase? sendingComponent, Event triggeredEvent, TMsg? data)
{
switch (triggeredEvent)
@@ -0,0 +1,23 @@
@inherits ConfigurationBase
@typeparam T
<MudSelectExtended
T="T"
MultiSelection="@true"
MultiSelectionTextFunc="@this.GetMultiSelectionText"
SelectedValues="@this.SelectedValues()"
Strict="@true"
Disabled="@this.Disabled()"
Margin="Margin.Dense"
Label="@this.OptionDescription"
Class="@GetClass"
Variant="Variant.Outlined"
HelperText="@this.OptionHelp"
SelectedValuesChanged="@this.OptionChanged">
@foreach (var data in this.Data)
{
<MudSelectItemExtended Value="@data.Value">
@data.Name
</MudSelectItemExtended>
}
</MudSelectExtended>
@@ -0,0 +1,54 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
/// <summary>
/// Configuration component for selecting many values from a list.
/// </summary>
/// <typeparam name="T">The type of the value to select.</typeparam>
public partial class ConfigurationMultiSelect<T> : ConfigurationBase
{
/// <summary>
/// The data to select from.
/// </summary>
[Parameter]
public IEnumerable<ConfigurationSelectData<T>> Data { get; set; } = [];
/// <summary>
/// The selected values.
/// </summary>
[Parameter]
public Func<HashSet<T>> SelectedValues { get; set; } = () => [];
/// <summary>
/// An action that is called when the selection changes.
/// </summary>
[Parameter]
public Action<HashSet<T>> SelectionUpdate { get; set; } = _ => { };
private async Task OptionChanged(IEnumerable<T?>? updatedValues)
{
if(updatedValues is null)
this.SelectionUpdate([]);
else
this.SelectionUpdate(updatedValues.Where(n => n is not null).ToHashSet()!);
await this.SettingsManager.StoreSettings();
await this.InformAboutChange();
}
private static string GetClass => $"{MARGIN_CLASS} rounded-lg";
private string GetMultiSelectionText(List<T?>? selectedValues)
{
if(selectedValues is null || selectedValues.Count == 0)
return "No preview features selected.";
if(selectedValues.Count == 1)
return $"You have selected 1 preview feature.";
return $"You have selected {selectedValues.Count} preview features.";
}
}
@@ -1,3 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using AIStudio.Provider;
using AIStudio.Settings;
@@ -47,8 +49,12 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
#endregion
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
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);
foreach (var providerId in this.Data)
{
@@ -60,6 +66,8 @@ public partial class ConfigurationProviderSelection : ComponentBase, IMessageBus
#region Implementation of IMessageBusReceiver
public string ComponentName => nameof(ConfigurationProviderSelection);
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
@@ -1,7 +1,7 @@
@inherits ConfigurationBase
@typeparam T
<MudSelect T="T" Value="@this.SelectedValue()" Strict="@true" Disabled="@this.Disabled()" Margin="Margin.Dense" Label="@this.OptionDescription" Class="@GetClass" Variant="Variant.Outlined" HelperText="@this.OptionHelp" ValueChanged="@this.OptionChanged">
<MudSelect T="T" Value="@this.SelectedValue()" Strict="@true" ShrinkLabel="@true" Disabled="@this.Disabled()" Margin="Margin.Dense" Label="@this.OptionDescription" Class="@GetClass" Variant="Variant.Outlined" HelperText="@this.OptionHelp" ValueChanged="@this.OptionChanged">
@foreach (var data in this.Data)
{
<MudSelectItem Value="@data.Value">
@@ -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; }
}
@@ -0,0 +1,135 @@
@using AIStudio.Settings
@if (this.SelectionMode is DataSourceSelectionMode.SELECTION_MODE)
{
<div class="d-flex">
<MudTooltip Text="Select the data sources 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 sources
</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/>
<MudText Typo="Typo.h5">Data Source Selection</MudText>
</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.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"/>
}
@if (this.aiBasedSourceSelection is false || this.DataSourcesAISelected.Count == 0)
{
<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>
}
else
{
<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>
}
}
}
</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>
}
@@ -0,0 +1,278 @@
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
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 ILogger<DataSourceSelection> Logger { 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 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
}
@@ -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,
}
@@ -1,7 +1,7 @@
@typeparam T
@inherits EnumSelectionBase
<MudStack Row="@true" AlignItems="AlignItems.Center" Class="mb-3">
<MudStack Row="@true" Class="mb-3">
<MudSelect T="@T" Value="@this.Value" ValueChanged="@this.SelectionChanged" AdornmentIcon="@this.Icon" Adornment="Adornment.Start" Label="@this.Label" Variant="Variant.Outlined" Margin="Margin.Dense" Validation="@this.ValidateSelection">
@foreach (var value in Enum.GetValues<T>())
{
@@ -1,17 +1,23 @@
@inherits MSGComponentBase
<div class="d-flex flex-column" style="@this.Height">
<div class="flex-auto overflow-auto">
<div class="@this.Classes" style="@this.Styles">
@if (this.HeaderContent is not null)
{
<div>
@this.HeaderContent
</div>
}
<div class="flex-auto overflow-auto mb-3">
@this.ChildContent
<div @ref="@this.AnchorAfterChildContent">
<div style="max-height: 0.1em;" @ref="@this.AnchorAfterChildContent">
&nbsp;
</div>
</div>
@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
</MudPaper>
}
@@ -6,13 +6,11 @@ namespace AIStudio.Components;
public partial class InnerScrolling : MSGComponentBase
{
/// <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";
public bool FillEntireHorizontalSpace { get; set; }
[Parameter]
public RenderFragment? HeaderContent { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
@@ -22,6 +20,15 @@ public partial class InnerScrolling : MSGComponentBase
/// </summary>
[Parameter]
public RenderFragment? FooterContent { get; set; }
[Parameter]
public string Class { get; set; } = string.Empty;
[Parameter]
public string? MinWidth { get; set; }
[Parameter]
public string Style { get; set; } = string.Empty;
[CascadingParameter]
private MainLayout MainLayout { get; set; } = null!;
@@ -43,6 +50,8 @@ public partial class InnerScrolling : 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
{
switch (triggeredEvent)
@@ -62,7 +71,13 @@ public partial class InnerScrolling : MSGComponentBase
#endregion
private string Height => $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight});";
private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth}; ";
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 Styles => $"flex-grow: 1; overflow: hidden; {this.TerminatedStyles}{this.MinWidthStyle}";
public async Task ScrollToBottom()
{
Loaded 100 of 979 files, more files were not shown because too many files have changed in this diff. Show more