diff --git a/.circleci/config.yml b/.circleci/config.yml index c6bba23d3346..d8ccf68f0141 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,6 +8,10 @@ workflows: generate-config: jobs: - path-filtering/filter: + filters: + tags: + only: + - /.*/ base-revision: main config-path: .circleci/continue_config.yml mapping: | diff --git a/.circleci/continue_config.yml b/.circleci/continue_config.yml index 1ca880307d72..88797ab2ebd1 100644 --- a/.circleci/continue_config.yml +++ b/.circleci/continue_config.yml @@ -22,10 +22,17 @@ jobs: # work around CircleCI-Public/path-filtering-orb#20 noop: docker: - - image: cimg/base:current + - image: cimg/base:stable steps: - run: "true" - + validate-commit-on-main: + docker: + - image: cimg/base:stable + steps: + - checkout + - run: + name: Verify that commit is on the main branch + command: git merge-base --is-ancestor HEAD main build-offline-chat-installer-macos: macos: xcode: 15.4.0 @@ -48,10 +55,15 @@ jobs: - run: name: Installing Qt command: | - wget "https://gpt4all.io/ci/qt-unified-macOS-x64-4.6.0-online.dmg" - hdiutil attach qt-unified-macOS-x64-4.6.0-online.dmg - /Volumes/qt-unified-macOS-x64-4.6.0-online/qt-unified-macOS-x64-4.6.0-online.app/Contents/MacOS/qt-unified-macOS-x64-4.6.0-online --no-force-installations --no-default-installations --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations --email $QT_EMAIL --password $QT_PASSWORD install qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.651.clang_64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qtpdf qt.qt6.651.addons.qthttpserver - hdiutil detach /Volumes/qt-unified-macOS-x64-4.6.0-online + wget "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-macOS-x64-4.8.1.dmg" + hdiutil attach qt-online-installer-macOS-x64-4.8.1.dmg + /Volumes/qt-online-installer-macOS-x64-4.8.1/qt-online-installer-macOS-x64-4.8.1.app/Contents/MacOS/qt-online-installer-macOS-x64-4.8.1 \ + --no-force-installations --no-default-installations --no-size-checking --default-answer \ + --accept-licenses --confirm-command --accept-obligations --email "$QT_EMAIL" --password "$QT_PASSWORD" \ + install \ + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.clang_64 qt.qt6.682.addons.qt5compat \ + extensions.qtpdf.682 qt.qt6.682.addons.qthttpserver + hdiutil detach /Volumes/qt-online-installer-macOS-x64-4.8.1 - run: name: Setup Keychain command: | @@ -72,17 +84,28 @@ jobs: ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake \ -S ../gpt4all-chat -B . -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH:PATH=~/Qt/6.5.1/macos/lib/cmake \ + -DCMAKE_PREFIX_PATH:PATH=~/Qt/6.8.2/macos/lib/cmake \ -DCMAKE_MAKE_PROGRAM:FILEPATH=~/Qt/Tools/Ninja/ninja \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DBUILD_UNIVERSAL=ON \ -DCMAKE_OSX_DEPLOYMENT_TARGET=12.6 \ -DGGML_METAL_MACOSX_VERSION_MIN=12.6 \ - -DMACDEPLOYQT=~/Qt/6.5.1/macos/bin/macdeployqt \ + -DMACDEPLOYQT=~/Qt/6.8.2/macos/bin/macdeployqt \ -DGPT4ALL_OFFLINE_INSTALLER=ON \ - -DGPT4ALL_SIGN_INSTALL=ON - ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake --build . --target all + -DGPT4ALL_SIGN_INSTALL=ON \ + -DGPT4ALL_GEN_CPACK_CONFIG=ON + ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake --build . --target package + ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake . -DGPT4ALL_GEN_CPACK_CONFIG=OFF + # The 'install' step here *should* be completely unnecessary. There is absolutely no reason we should have + # to copy all of the build artifacts to an output directory that we do not use (because we package GPT4All + # as an installer instead). + # However, because of the way signing is implemented in the cmake script, the *source* files are signed at + # install time instead of the *installed* files. This side effect is the *only* way libraries that are not + # processed by macdeployqt, such as libllmodel.so, get signed (at least, with -DBUILD_UNIVERSAL=ON). + # Also, we have to run this as a *separate* step. Telling cmake to run both targets in one command causes it + # to execute them in parallel, since it is not aware of the dependency of the package target on the install + # target. ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake --build . --target install ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake --build . --target package ccache -s @@ -163,6 +186,21 @@ jobs: xcrun stapler staple build/upload/gpt4all-installer-darwin-signed.dmg - store_artifacts: path: build/upload + - run: + name: Install Rosetta + command: softwareupdate --install-rosetta --agree-to-license # needed for QtIFW + - run: + name: Test installation and verify that it is signed + command: | + set -e + hdiutil attach build/upload/gpt4all-installer-darwin-signed.dmg + codesign --verify --deep --verbose /Volumes/gpt4all-installer-darwin/gpt4all-installer-darwin.app + /Volumes/gpt4all-installer-darwin/gpt4all-installer-darwin.app/Contents/MacOS/gpt4all-installer-darwin \ + --no-size-checking --default-answer --accept-licenses --confirm-command \ + install gpt4all + codesign --verify --deep --verbose /Applications/gpt4all/bin/gpt4all.app + codesign --verify --deep --verbose /Applications/gpt4all/maintenancetool.app + hdiutil detach /Volumes/gpt4all-installer-darwin build-online-chat-installer-macos: macos: @@ -186,10 +224,15 @@ jobs: - run: name: Installing Qt command: | - wget "https://gpt4all.io/ci/qt-unified-macOS-x64-4.6.0-online.dmg" - hdiutil attach qt-unified-macOS-x64-4.6.0-online.dmg - /Volumes/qt-unified-macOS-x64-4.6.0-online/qt-unified-macOS-x64-4.6.0-online.app/Contents/MacOS/qt-unified-macOS-x64-4.6.0-online --no-force-installations --no-default-installations --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations --email $QT_EMAIL --password $QT_PASSWORD install qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.651.clang_64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qtpdf qt.qt6.651.addons.qthttpserver - hdiutil detach /Volumes/qt-unified-macOS-x64-4.6.0-online + wget "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-macOS-x64-4.8.1.dmg" + hdiutil attach qt-online-installer-macOS-x64-4.8.1.dmg + /Volumes/qt-online-installer-macOS-x64-4.8.1/qt-online-installer-macOS-x64-4.8.1.app/Contents/MacOS/qt-online-installer-macOS-x64-4.8.1 \ + --no-force-installations --no-default-installations --no-size-checking --default-answer \ + --accept-licenses --confirm-command --accept-obligations --email "$QT_EMAIL" --password "$QT_PASSWORD" \ + install \ + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.clang_64 qt.qt6.682.addons.qt5compat \ + extensions.qtpdf.682 qt.qt6.682.addons.qthttpserver + hdiutil detach /Volumes/qt-online-installer-macOS-x64-4.8.1 - run: name: Setup Keychain command: | @@ -210,17 +253,20 @@ jobs: ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake \ -S ../gpt4all-chat -B . -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH:PATH=~/Qt/6.5.1/macos/lib/cmake \ + -DCMAKE_PREFIX_PATH:PATH=~/Qt/6.8.2/macos/lib/cmake \ -DCMAKE_MAKE_PROGRAM:FILEPATH=~/Qt/Tools/Ninja/ninja \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ -DBUILD_UNIVERSAL=ON \ -DCMAKE_OSX_DEPLOYMENT_TARGET=12.6 \ -DGGML_METAL_MACOSX_VERSION_MIN=12.6 \ - -DMACDEPLOYQT=~/Qt/6.5.1/macos/bin/macdeployqt \ + -DMACDEPLOYQT=~/Qt/6.8.2/macos/bin/macdeployqt \ -DGPT4ALL_OFFLINE_INSTALLER=OFF \ - -DGPT4ALL_SIGN_INSTALL=ON - ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake --build . --target all + -DGPT4ALL_SIGN_INSTALL=ON \ + -DGPT4ALL_GEN_CPACK_CONFIG=ON + ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake --build . --target package + ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake . -DGPT4ALL_GEN_CPACK_CONFIG=OFF + # See comment above related to the 'install' target. ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake --build . --target install ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake --build . --target package ccache -s @@ -302,6 +348,22 @@ jobs: xcrun stapler staple build/upload/gpt4all-installer-darwin-signed.dmg - store_artifacts: path: build/upload + - run: + name: Install Rosetta + command: softwareupdate --install-rosetta --agree-to-license # needed for QtIFW + - run: + name: Test installation and verify that it is signed + command: | + set -e + hdiutil attach build/upload/gpt4all-installer-darwin-signed.dmg + codesign --verify --deep --verbose /Volumes/gpt4all-installer-darwin/gpt4all-installer-darwin.app + tar -xf build/upload/repository.tar.gz + /Volumes/gpt4all-installer-darwin/gpt4all-installer-darwin.app/Contents/MacOS/gpt4all-installer-darwin \ + --no-size-checking --default-answer --accept-licenses --confirm-command --set-temp-repository repository \ + install gpt4all + codesign --verify --deep --verbose /Applications/gpt4all/bin/gpt4all.app + codesign --verify --deep --verbose /Applications/gpt4all/maintenancetool.app + hdiutil detach /Volumes/gpt4all-installer-darwin build-offline-chat-installer-linux: machine: @@ -319,6 +381,8 @@ jobs: - run: name: Setup Linux and Dependencies command: | + # Prevent apt-get from interactively prompting for service restart + echo "\$nrconf{restart} = 'l'" | sudo tee /etc/needrestart/conf.d/90-autorestart.conf >/dev/null wget -qO- "https://packages.lunarg.com/lunarg-signing-key-pub.asc" | sudo tee /etc/apt/trusted.gpg.d/lunarg.asc wget -qO- "https://packages.lunarg.com/vulkan/1.3.290/lunarg-vulkan-1.3.290-jammy.list" | sudo tee /etc/apt/sources.list.d/lunarg-vulkan-1.3.290-jammy.list wget "https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb" @@ -336,9 +400,13 @@ jobs: - run: name: Installing Qt command: | - wget "https://gpt4all.io/ci/qt-unified-linux-x64-4.6.0-online.run" - chmod +x qt-unified-linux-x64-4.6.0-online.run - ./qt-unified-linux-x64-4.6.0-online.run --no-force-installations --no-default-installations --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations --email $QT_EMAIL --password $QT_PASSWORD install qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.651.gcc_64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qtpdf qt.qt6.651.addons.qthttpserver + wget "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-linux-x64-4.8.1.run" + chmod +x qt-online-installer-linux-x64-4.8.1.run + ./qt-online-installer-linux-x64-4.8.1.run --no-force-installations --no-default-installations \ + --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations \ + --email "$QT_EMAIL" --password "$QT_PASSWORD" install \ + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.linux_gcc_64 qt.qt6.682.addons.qt5compat \ + qt.qt6.682.debug_info extensions.qtpdf.682 qt.qt6.682.addons.qthttpserver - run: name: Build linuxdeployqt command: | @@ -349,7 +417,7 @@ jobs: no_output_timeout: 30m command: | set -eo pipefail - export CMAKE_PREFIX_PATH=~/Qt/6.5.1/gcc_64/lib/cmake + export CMAKE_PREFIX_PATH=~/Qt/6.8.2/gcc_64/lib/cmake export PATH=$PATH:$HOME/Qt/Tools/QtInstallerFramework/4.8/bin export PATH=$PATH:/usr/local/cuda/bin ccache -o "cache_dir=${PWD}/../.ccache" -o max_size=500M -p -z @@ -378,6 +446,13 @@ jobs: when: always paths: - ../.ccache + - run: + name: Test installation + command: | + mkdir ~/Desktop + build/upload/gpt4all-installer-linux.run --no-size-checking --default-answer --accept-licenses \ + --confirm-command \ + install gpt4all build-online-chat-installer-linux: machine: @@ -395,6 +470,8 @@ jobs: - run: name: Setup Linux and Dependencies command: | + # Prevent apt-get from interactively prompting for service restart + echo "\$nrconf{restart} = 'l'" | sudo tee /etc/needrestart/conf.d/90-autorestart.conf >/dev/null wget -qO- "https://packages.lunarg.com/lunarg-signing-key-pub.asc" | sudo tee /etc/apt/trusted.gpg.d/lunarg.asc wget -qO- "https://packages.lunarg.com/vulkan/1.3.290/lunarg-vulkan-1.3.290-jammy.list" | sudo tee /etc/apt/sources.list.d/lunarg-vulkan-1.3.290-jammy.list wget "https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb" @@ -412,9 +489,13 @@ jobs: - run: name: Installing Qt command: | - wget "https://gpt4all.io/ci/qt-unified-linux-x64-4.6.0-online.run" - chmod +x qt-unified-linux-x64-4.6.0-online.run - ./qt-unified-linux-x64-4.6.0-online.run --no-force-installations --no-default-installations --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations --email $QT_EMAIL --password $QT_PASSWORD install qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.651.gcc_64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qtpdf qt.qt6.651.addons.qthttpserver + wget "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-linux-x64-4.8.1.run" + chmod +x qt-online-installer-linux-x64-4.8.1.run + ./qt-online-installer-linux-x64-4.8.1.run --no-force-installations --no-default-installations \ + --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations \ + --email "$QT_EMAIL" --password "$QT_PASSWORD" install \ + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.linux_gcc_64 qt.qt6.682.addons.qt5compat \ + qt.qt6.682.debug_info extensions.qtpdf.682 qt.qt6.682.addons.qthttpserver - run: name: Build linuxdeployqt command: | @@ -425,7 +506,7 @@ jobs: no_output_timeout: 30m command: | set -eo pipefail - export CMAKE_PREFIX_PATH=~/Qt/6.5.1/gcc_64/lib/cmake + export CMAKE_PREFIX_PATH=~/Qt/6.8.2/gcc_64/lib/cmake export PATH=$PATH:$HOME/Qt/Tools/QtInstallerFramework/4.8/bin export PATH=$PATH:/usr/local/cuda/bin ccache -o "cache_dir=${PWD}/../.ccache" -o max_size=500M -p -z @@ -455,10 +536,19 @@ jobs: when: always paths: - ../.ccache + - run: + name: Test installation + command: | + mkdir ~/Desktop + build/upload/gpt4all-installer-linux.run --no-size-checking --default-answer --accept-licenses \ + --confirm-command \ + --set-temp-repository build/_CPack_Packages/Linux/IFW/gpt4all-installer-linux/repository \ + install gpt4all build-offline-chat-installer-windows: machine: - image: windows-server-2022-gui:current + # we use 2024.04.01 because nvcc complains about the MSVC ver if we use anything newer + image: windows-server-2022-gui:2024.04.1 resource_class: windows.large shell: powershell.exe -ExecutionPolicy Bypass steps: @@ -477,8 +567,12 @@ jobs: - run: name: Installing Qt command: | - wget.exe "https://gpt4all.io/ci/qt-unified-windows-x64-4.6.0-online.exe" - & .\qt-unified-windows-x64-4.6.0-online.exe --no-force-installations --no-default-installations --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations --email ${Env:QT_EMAIL} --password ${Env:QT_PASSWORD} install qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.651.win64_msvc2019_64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qtpdf qt.qt6.651.addons.qthttpserver + wget.exe "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-windows-x64-4.8.1.exe" + & .\qt-online-installer-windows-x64-4.8.1.exe --no-force-installations --no-default-installations ` + --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations ` + --email "${Env:QT_EMAIL}" --password "${Env:QT_PASSWORD}" install ` + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.win64_msvc2022_64 qt.qt6.682.addons.qt5compat ` + qt.qt6.682.debug_info extensions.qtpdf.682 qt.qt6.682.addons.qthttpserver - run: name: Install VulkanSDK command: | @@ -495,7 +589,7 @@ jobs: mkdir dotnet cd dotnet $dotnet_url="https://download.visualstudio.microsoft.com/download/pr/5af098e1-e433-4fda-84af-3f54fd27c108/6bd1c6e48e64e64871957289023ca590/dotnet-sdk-8.0.302-win-x64.zip" - wget "$dotnet_url" + wget.exe "$dotnet_url" Expand-Archive -LiteralPath .\dotnet-sdk-8.0.302-win-x64.zip $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet-sdk-8.0.302-win-x64" $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" @@ -519,7 +613,7 @@ jobs: & "C:\Qt\Tools\CMake_64\bin\cmake.exe" ` -S ..\gpt4all-chat -B . -G Ninja ` -DCMAKE_BUILD_TYPE=Release ` - "-DCMAKE_PREFIX_PATH:PATH=C:\Qt\6.5.1\msvc2019_64" ` + "-DCMAKE_PREFIX_PATH:PATH=C:\Qt\6.8.2\msvc2022_64" ` "-DCMAKE_MAKE_PROGRAM:FILEPATH=C:\Qt\Tools\Ninja\ninja.exe" ` -DCMAKE_C_COMPILER_LAUNCHER=ccache ` -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ` @@ -549,7 +643,7 @@ jobs: sign-offline-chat-installer-windows: machine: - image: windows-server-2022-gui:current + image: windows-server-2022-gui:2024.04.1 resource_class: windows.large shell: powershell.exe -ExecutionPolicy Bypass steps: @@ -579,10 +673,16 @@ jobs: AzureSignTool.exe sign -du "https://gpt4all.io/index.html" -kvu https://gpt4all.vault.azure.net -kvi "$Env:AZSignGUID" -kvs "$Env:AZSignPWD" -kvc "$Env:AZSignCertName" -kvt "$Env:AZSignTID" -tr http://timestamp.digicert.com -v "$($(Get-Location).Path)\build\upload\gpt4all-installer-win64.exe" - store_artifacts: path: build/upload + - run: + name: Test installation + command: | + build\upload\gpt4all-installer-win64.exe --no-size-checking --default-answer --accept-licenses ` + --confirm-command ` + install gpt4all build-online-chat-installer-windows: machine: - image: windows-server-2022-gui:current + image: windows-server-2022-gui:2024.04.1 resource_class: windows.large shell: powershell.exe -ExecutionPolicy Bypass steps: @@ -601,8 +701,12 @@ jobs: - run: name: Installing Qt command: | - wget.exe "https://gpt4all.io/ci/qt-unified-windows-x64-4.6.0-online.exe" - & .\qt-unified-windows-x64-4.6.0-online.exe --no-force-installations --no-default-installations --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations --email ${Env:QT_EMAIL} --password ${Env:QT_PASSWORD} install qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.651.win64_msvc2019_64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qtpdf qt.qt6.651.addons.qthttpserver + wget.exe "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-windows-x64-4.8.1.exe" + & .\qt-online-installer-windows-x64-4.8.1.exe --no-force-installations --no-default-installations ` + --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations ` + --email "${Env:QT_EMAIL}" --password "${Env:QT_PASSWORD}" install ` + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.win64_msvc2022_64 qt.qt6.682.addons.qt5compat ` + qt.qt6.682.debug_info extensions.qtpdf.682 qt.qt6.682.addons.qthttpserver - run: name: Install VulkanSDK command: | @@ -648,7 +752,7 @@ jobs: & "C:\Qt\Tools\CMake_64\bin\cmake.exe" ` -S ..\gpt4all-chat -B . -G Ninja ` -DCMAKE_BUILD_TYPE=Release ` - "-DCMAKE_PREFIX_PATH:PATH=C:\Qt\6.5.1\msvc2019_64" ` + "-DCMAKE_PREFIX_PATH:PATH=C:\Qt\6.8.2\msvc2022_64" ` "-DCMAKE_MAKE_PROGRAM:FILEPATH=C:\Qt\Tools\Ninja\ninja.exe" ` -DCMAKE_C_COMPILER_LAUNCHER=ccache ` -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ` @@ -680,7 +784,7 @@ jobs: sign-online-chat-installer-windows: machine: - image: windows-server-2022-gui:current + image: windows-server-2022-gui:2024.04.1 resource_class: windows.large shell: powershell.exe -ExecutionPolicy Bypass steps: @@ -715,6 +819,281 @@ jobs: AzureSignTool.exe sign -du "https://gpt4all.io/index.html" -kvu https://gpt4all.vault.azure.net -kvi "$Env:AZSignGUID" -kvs "$Env:AZSignPWD" -kvc "$Env:AZSignCertName" -kvt "$Env:AZSignTID" -tr http://timestamp.digicert.com -v "$($(Get-Location).Path)/build/upload/gpt4all-installer-win64.exe" - store_artifacts: path: build/upload + - run: + name: Test installation + command: | + Expand-Archive -LiteralPath build\upload\repository.zip -DestinationPath . + build\upload\gpt4all-installer-win64.exe --no-size-checking --default-answer --accept-licenses ` + --confirm-command --set-temp-repository repository ` + install gpt4all + + build-offline-chat-installer-windows-arm: + machine: + # we use 2024.04.01 because nvcc complains about the MSVC ver if we use anything newer + image: windows-server-2022-gui:2024.04.1 + resource_class: windows.large + shell: powershell.exe -ExecutionPolicy Bypass + steps: + - checkout + - run: + name: Update Submodules + command: | + git submodule sync + git submodule update --init --recursive + - restore_cache: + keys: + - ccache-gpt4all-win-aarch64- + - run: + name: Install dependencies + command: choco install -y ccache wget + - run: + name: Installing Qt + command: | + wget.exe "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-windows-x64-4.8.1.exe" + & .\qt-online-installer-windows-x64-4.8.1.exe --no-force-installations --no-default-installations ` + --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations ` + --email "${Env:QT_EMAIL}" --password "${Env:QT_PASSWORD}" install ` + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.win64_msvc2022_64 ` + qt.qt6.682.win64_msvc2022_arm64_cross_compiled qt.qt6.682.addons.qt5compat qt.qt6.682.debug_info ` + qt.qt6.682.addons.qthttpserver + - run: + name: "Install Dotnet 8" + command: | + mkdir dotnet + cd dotnet + $dotnet_url="https://download.visualstudio.microsoft.com/download/pr/5af098e1-e433-4fda-84af-3f54fd27c108/6bd1c6e48e64e64871957289023ca590/dotnet-sdk-8.0.302-win-x64.zip" + wget.exe "$dotnet_url" + Expand-Archive -LiteralPath .\dotnet-sdk-8.0.302-win-x64.zip + $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet-sdk-8.0.302-win-x64" + $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" + $Env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=$true + dotnet tool install --global AzureSignTool + - run: + name: Build + no_output_timeout: 30m + command: | + $vsInstallPath = & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -property installationpath + Import-Module "${vsInstallPath}\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" + Enter-VsDevShell -VsInstallPath "$vsInstallPath" -SkipAutomaticLocation -Arch arm64 -HostArch amd64 -DevCmdArguments '-no_logo' + + $Env:PATH = "${Env:PATH};C:\Qt\Tools\QtInstallerFramework\4.8\bin" + $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet\dotnet-sdk-8.0.302-win-x64" + $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" + ccache -o "cache_dir=${pwd}\..\.ccache" -o max_size=500M -p -z + mkdir build + cd build + & "C:\Qt\Tools\CMake_64\bin\cmake.exe" ` + -S ..\gpt4all-chat -B . -G Ninja ` + -DCMAKE_BUILD_TYPE=Release ` + "-DCMAKE_PREFIX_PATH:PATH=C:\Qt\6.8.2\msvc2022_arm64" ` + "-DCMAKE_MAKE_PROGRAM:FILEPATH=C:\Qt\Tools\Ninja\ninja.exe" ` + "-DCMAKE_TOOLCHAIN_FILE=C:\Qt\6.8.2\msvc2022_arm64\lib\cmake\Qt6\qt.toolchain.cmake" ` + -DCMAKE_C_COMPILER_LAUNCHER=ccache ` + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ` + -DLLMODEL_CUDA=OFF ` + -DLLMODEL_KOMPUTE=OFF ` + "-DWINDEPLOYQT=C:\Qt\6.8.2\msvc2022_64\bin\windeployqt.exe;--qtpaths;C:\Qt\6.8.2\msvc2022_arm64\bin\qtpaths.bat" ` + -DGPT4ALL_TEST=OFF ` + -DGPT4ALL_OFFLINE_INSTALLER=ON + & "C:\Qt\Tools\Ninja\ninja.exe" + & "C:\Qt\Tools\Ninja\ninja.exe" install + & "C:\Qt\Tools\Ninja\ninja.exe" package + ccache -s + mkdir upload + copy gpt4all-installer-win64-arm.exe upload + - store_artifacts: + path: build/upload + # add workspace so signing jobs can connect & obtain dmg + - save_cache: + key: ccache-gpt4all-win-aarch64-{{ epoch }} + when: always + paths: + - ..\.ccache + - persist_to_workspace: + root: build + # specify path to only include components we want to persist + # accross builds + paths: + - upload + + sign-offline-chat-installer-windows-arm: + machine: + image: windows-server-2022-gui:2024.04.1 + resource_class: windows.large + shell: powershell.exe -ExecutionPolicy Bypass + steps: + - checkout + - attach_workspace: + at: build + - run: + name: Install dependencies + command: choco install -y wget + - run: + name: "Install Dotnet 8 && Azure Sign Tool" + command: | + mkdir dotnet + cd dotnet + $dotnet_url="https://download.visualstudio.microsoft.com/download/pr/5af098e1-e433-4fda-84af-3f54fd27c108/6bd1c6e48e64e64871957289023ca590/dotnet-sdk-8.0.302-win-x64.zip" + wget.exe "$dotnet_url" + Expand-Archive -LiteralPath .\dotnet-sdk-8.0.302-win-x64.zip + $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet-sdk-8.0.302-win-x64" + $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" + $Env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=$true + dotnet tool install --global AzureSignTool + - run: + name: "Sign Windows Installer With AST" + command: | + $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet\dotnet-sdk-8.0.302-win-x64" + $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" + AzureSignTool.exe sign -du "https://gpt4all.io/index.html" -kvu https://gpt4all.vault.azure.net -kvi "$Env:AZSignGUID" -kvs "$Env:AZSignPWD" -kvc "$Env:AZSignCertName" -kvt "$Env:AZSignTID" -tr http://timestamp.digicert.com -v "$($(Get-Location).Path)\build\upload\gpt4all-installer-win64-arm.exe" + - store_artifacts: + path: build/upload + - run: + name: Test installation + command: | + build\upload\gpt4all-installer-win64-arm.exe --no-size-checking --default-answer --accept-licenses ` + --confirm-command ` + install gpt4all + + build-online-chat-installer-windows-arm: + machine: + image: windows-server-2022-gui:2024.04.1 + resource_class: windows.large + shell: powershell.exe -ExecutionPolicy Bypass + steps: + - checkout + - run: + name: Update Submodules + command: | + git submodule sync + git submodule update --init --recursive + - restore_cache: + keys: + - ccache-gpt4all-win-aarch64- + - run: + name: Install dependencies + command: choco install -y ccache wget + - run: + name: Installing Qt + command: | + wget.exe "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-windows-x64-4.8.1.exe" + & .\qt-online-installer-windows-x64-4.8.1.exe --no-force-installations --no-default-installations ` + --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations ` + --email "${Env:QT_EMAIL}" --password "${Env:QT_PASSWORD}" install ` + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.win64_msvc2022_64 ` + qt.qt6.682.win64_msvc2022_arm64_cross_compiled qt.qt6.682.addons.qt5compat qt.qt6.682.debug_info ` + qt.qt6.682.addons.qthttpserver + - run: + name: "Install Dotnet 8" + command: | + mkdir dotnet + cd dotnet + $dotnet_url="https://download.visualstudio.microsoft.com/download/pr/5af098e1-e433-4fda-84af-3f54fd27c108/6bd1c6e48e64e64871957289023ca590/dotnet-sdk-8.0.302-win-x64.zip" + wget.exe "$dotnet_url" + Expand-Archive -LiteralPath .\dotnet-sdk-8.0.302-win-x64.zip + $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet-sdk-8.0.302-win-x64" + $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" + - run: + name: "Setup Azure SignTool" + command: | + $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet\dotnet-sdk-8.0.302-win-x64" + $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" + $Env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=$true + dotnet tool install --global AzureSignTool + - run: + name: Build + no_output_timeout: 30m + command: | + $vsInstallPath = & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -property installationpath + Import-Module "${vsInstallPath}\Common7\Tools\Microsoft.VisualStudio.DevShell.dll" + Enter-VsDevShell -VsInstallPath "$vsInstallPath" -SkipAutomaticLocation -Arch arm64 -HostArch amd64 -DevCmdArguments '-no_logo' + + $Env:PATH = "${Env:PATH};C:\Qt\Tools\QtInstallerFramework\4.8\bin" + $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet\dotnet-sdk-8.0.302-win-x64" + $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" + ccache -o "cache_dir=${pwd}\..\.ccache" -o max_size=500M -p -z + mkdir build + cd build + & "C:\Qt\Tools\CMake_64\bin\cmake.exe" ` + -S ..\gpt4all-chat -B . -G Ninja ` + -DCMAKE_BUILD_TYPE=Release ` + "-DCMAKE_PREFIX_PATH:PATH=C:\Qt\6.8.2\msvc2022_arm64" ` + "-DCMAKE_MAKE_PROGRAM:FILEPATH=C:\Qt\Tools\Ninja\ninja.exe" ` + "-DCMAKE_TOOLCHAIN_FILE=C:\Qt\6.8.2\msvc2022_arm64\lib\cmake\Qt6\qt.toolchain.cmake" ` + -DCMAKE_C_COMPILER_LAUNCHER=ccache ` + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ` + -DLLMODEL_CUDA=OFF ` + -DLLMODEL_KOMPUTE=OFF ` + "-DWINDEPLOYQT=C:\Qt\6.8.2\msvc2022_64\bin\windeployqt.exe;--qtpaths;C:\Qt\6.8.2\msvc2022_arm64\bin\qtpaths.bat" ` + -DGPT4ALL_TEST=OFF ` + -DGPT4ALL_OFFLINE_INSTALLER=OFF + & "C:\Qt\Tools\Ninja\ninja.exe" + & "C:\Qt\Tools\Ninja\ninja.exe" install + & "C:\Qt\Tools\Ninja\ninja.exe" package + ccache -s + mkdir upload + copy gpt4all-installer-win64-arm.exe upload + Set-Location -Path "_CPack_Packages/win64/IFW/gpt4all-installer-win64-arm" + Compress-Archive -Path 'repository' -DestinationPath '..\..\..\..\upload\repository.zip' + - store_artifacts: + path: build/upload + - save_cache: + key: ccache-gpt4all-win-aarch64-{{ epoch }} + when: always + paths: + - ..\.ccache + # add workspace so signing jobs can connect & obtain dmg + - persist_to_workspace: + root: build + # specify path to only include components we want to persist + # accross builds + paths: + - upload + + sign-online-chat-installer-windows-arm: + machine: + image: windows-server-2022-gui:2024.04.1 + resource_class: windows.large + shell: powershell.exe -ExecutionPolicy Bypass + steps: + - checkout + - attach_workspace: + at: build + - run: + name: Install dependencies + command: choco install -y wget + - run: + name: "Install Dotnet 8" + command: | + mkdir dotnet + cd dotnet + $dotnet_url="https://download.visualstudio.microsoft.com/download/pr/5af098e1-e433-4fda-84af-3f54fd27c108/6bd1c6e48e64e64871957289023ca590/dotnet-sdk-8.0.302-win-x64.zip" + wget.exe "$dotnet_url" + Expand-Archive -LiteralPath .\dotnet-sdk-8.0.302-win-x64.zip + $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet-sdk-8.0.302-win-x64" + $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" + - run: + name: "Setup Azure SignTool" + command: | + $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet\dotnet-sdk-8.0.302-win-x64" + $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" + $Env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE=$true + dotnet tool install --global AzureSignTool + - run: + name: "Sign Windows Installer With AST" + command: | + $Env:DOTNET_ROOT="$($(Get-Location).Path)\dotnet\dotnet-sdk-8.0.302-win-x64" + $Env:PATH="$Env:DOTNET_ROOT;$Env:PATH" + AzureSignTool.exe sign -du "https://gpt4all.io/index.html" -kvu https://gpt4all.vault.azure.net -kvi "$Env:AZSignGUID" -kvs "$Env:AZSignPWD" -kvc "$Env:AZSignCertName" -kvt "$Env:AZSignTID" -tr http://timestamp.digicert.com -v "$($(Get-Location).Path)/build/upload/gpt4all-installer-win64-arm.exe" + - store_artifacts: + path: build/upload + - run: + name: Test installation + command: | + Expand-Archive -LiteralPath build\upload\repository.zip -DestinationPath . + build\upload\gpt4all-installer-win64-arm.exe --no-size-checking --default-answer --accept-licenses ` + --confirm-command --set-temp-repository repository ` + install gpt4all build-gpt4all-chat-linux: machine: @@ -732,6 +1111,8 @@ jobs: - run: name: Setup Linux and Dependencies command: | + # Prevent apt-get from interactively prompting for service restart + echo "\$nrconf{restart} = 'l'" | sudo tee /etc/needrestart/conf.d/90-autorestart.conf >/dev/null wget -qO- "https://packages.lunarg.com/lunarg-signing-key-pub.asc" | sudo tee /etc/apt/trusted.gpg.d/lunarg.asc wget -qO- "https://packages.lunarg.com/vulkan/1.3.290/lunarg-vulkan-1.3.290-jammy.list" | sudo tee /etc/apt/sources.list.d/lunarg-vulkan-1.3.290-jammy.list wget "https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb" @@ -749,14 +1130,18 @@ jobs: - run: name: Installing Qt command: | - wget "https://gpt4all.io/ci/qt-unified-linux-x64-4.6.0-online.run" - chmod +x qt-unified-linux-x64-4.6.0-online.run - ./qt-unified-linux-x64-4.6.0-online.run --no-force-installations --no-default-installations --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations --email $QT_EMAIL --password $QT_PASSWORD install qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.651.gcc_64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qtpdf qt.qt6.651.addons.qthttpserver + wget "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-linux-x64-4.8.1.run" + chmod +x qt-online-installer-linux-x64-4.8.1.run + ./qt-online-installer-linux-x64-4.8.1.run --no-force-installations --no-default-installations \ + --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations \ + --email "$QT_EMAIL" --password "$QT_PASSWORD" install \ + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.linux_gcc_64 qt.qt6.682.addons.qt5compat \ + qt.qt6.682.debug_info extensions.qtpdf.682 qt.qt6.682.addons.qthttpserver - run: name: Build no_output_timeout: 30m command: | - export CMAKE_PREFIX_PATH=~/Qt/6.5.1/gcc_64/lib/cmake + export CMAKE_PREFIX_PATH=~/Qt/6.8.2/gcc_64/lib/cmake export PATH=$PATH:/usr/local/cuda/bin ccache -o "cache_dir=${PWD}/../.ccache" -o max_size=500M -p -z ~/Qt/Tools/CMake/bin/cmake \ @@ -778,7 +1163,7 @@ jobs: build-gpt4all-chat-windows: machine: - image: windows-server-2022-gui:current + image: windows-server-2022-gui:2024.04.1 resource_class: windows.large shell: powershell.exe -ExecutionPolicy Bypass steps: @@ -797,8 +1182,12 @@ jobs: - run: name: Installing Qt command: | - wget.exe "https://gpt4all.io/ci/qt-unified-windows-x64-4.6.0-online.exe" - & .\qt-unified-windows-x64-4.6.0-online.exe --no-force-installations --no-default-installations --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations --email ${Env:QT_EMAIL} --password ${Env:QT_PASSWORD} install qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.651.win64_msvc2019_64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qtpdf qt.qt6.651.addons.qthttpserver + wget.exe "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-windows-x64-4.8.1.exe" + & .\qt-online-installer-windows-x64-4.8.1.exe --no-force-installations --no-default-installations ` + --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations ` + --email "${Env:QT_EMAIL}" --password "${Env:QT_PASSWORD}" install ` + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.win64_msvc2022_64 qt.qt6.682.addons.qt5compat ` + qt.qt6.682.debug_info extensions.qtpdf.682 qt.qt6.682.addons.qthttpserver - run: name: Install VulkanSDK command: | @@ -823,7 +1212,7 @@ jobs: & "C:\Qt\Tools\CMake_64\bin\cmake.exe" ` -S gpt4all-chat -B build -G Ninja ` -DCMAKE_BUILD_TYPE=Release ` - "-DCMAKE_PREFIX_PATH:PATH=C:\Qt\6.5.1\msvc2019_64" ` + "-DCMAKE_PREFIX_PATH:PATH=C:\Qt\6.8.2\msvc2022_64" ` "-DCMAKE_MAKE_PROGRAM:FILEPATH=C:\Qt\Tools\Ninja\ninja.exe" ` -DCMAKE_C_COMPILER_LAUNCHER=ccache ` -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ` @@ -859,10 +1248,15 @@ jobs: - run: name: Installing Qt command: | - wget "https://gpt4all.io/ci/qt-unified-macOS-x64-4.6.0-online.dmg" - hdiutil attach qt-unified-macOS-x64-4.6.0-online.dmg - /Volumes/qt-unified-macOS-x64-4.6.0-online/qt-unified-macOS-x64-4.6.0-online.app/Contents/MacOS/qt-unified-macOS-x64-4.6.0-online --no-force-installations --no-default-installations --no-size-checking --default-answer --accept-licenses --confirm-command --accept-obligations --email $QT_EMAIL --password $QT_PASSWORD install qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.651.clang_64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qtpdf qt.qt6.651.addons.qthttpserver - hdiutil detach /Volumes/qt-unified-macOS-x64-4.6.0-online + wget "https://qt.mirror.constant.com/archive/online_installers/4.8/qt-online-installer-macOS-x64-4.8.1.dmg" + hdiutil attach qt-online-installer-macOS-x64-4.8.1.dmg + /Volumes/qt-online-installer-macOS-x64-4.8.1/qt-online-installer-macOS-x64-4.8.1.app/Contents/MacOS/qt-online-installer-macOS-x64-4.8.1 \ + --no-force-installations --no-default-installations --no-size-checking --default-answer \ + --accept-licenses --confirm-command --accept-obligations --email "$QT_EMAIL" --password "$QT_PASSWORD" \ + install \ + qt.tools.cmake qt.tools.ifw.48 qt.tools.ninja qt.qt6.682.clang_64 qt.qt6.682.addons.qt5compat \ + extensions.qtpdf.682 qt.qt6.682.addons.qthttpserver + hdiutil detach /Volumes/qt-online-installer-macOS-x64-4.8.1 - run: name: Build no_output_timeout: 30m @@ -871,7 +1265,7 @@ jobs: ~/Qt/Tools/CMake/CMake.app/Contents/bin/cmake \ -S gpt4all-chat -B build -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_PREFIX_PATH:PATH=~/Qt/6.5.1/macos/lib/cmake \ + -DCMAKE_PREFIX_PATH:PATH=~/Qt/6.8.2/macos/lib/cmake \ -DCMAKE_MAKE_PROGRAM:FILEPATH=~/Qt/Tools/Ninja/ninja \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ @@ -905,7 +1299,7 @@ jobs: cd gpt4all-bindings/typescript npm run docs:build - build-py-docs: + deploy-docs: docker: - image: circleci/python:3.8 steps: @@ -1041,7 +1435,7 @@ jobs: build-py-windows: machine: - image: windows-server-2022-gui:current + image: windows-server-2022-gui:2024.04.1 resource_class: windows.large shell: powershell.exe -ExecutionPolicy Bypass steps: @@ -1109,7 +1503,7 @@ jobs: paths: - "*.whl" - store-and-upload-wheels: + deploy-wheels: docker: - image: circleci/python:3.8 steps: @@ -1238,7 +1632,7 @@ jobs: build-bindings-backend-windows: machine: - image: windows-server-2022-gui:current + image: windows-server-2022-gui:2024.04.1 resource_class: windows.large shell: powershell.exe -ExecutionPolicy Bypass steps: @@ -1406,7 +1800,7 @@ jobs: - prebuilds/win32-x64/*.node - runtimes/win32-x64/*-*.dll - prepare-npm-pkg: + deploy-npm-pkg: docker: - image: cimg/base:stable steps: @@ -1462,6 +1856,27 @@ jobs: npm set //registry.npmjs.org/:_authToken=$NPM_TOKEN npm publish +# only run a job on the main branch +job_only_main: &job_only_main + filters: + branches: + only: main + +# allow a job to run on tags as well as commits +job_allow_tags: &job_allow_tags + filters: + tags: + only: + - /.*/ + +# standard chat workflow filter +workflow-when-chat-requested: &workflow-when-chat-requested + when: + and: + - or: [ << pipeline.parameters.run-all-workflows >>, << pipeline.parameters.run-chat-workflow >> ] + - not: + equal: [ << pipeline.trigger_source >>, scheduled_pipeline ] + workflows: version: 2 noop: @@ -1472,125 +1887,265 @@ workflows: - << pipeline.parameters.run-python-workflow >> - << pipeline.parameters.run-ts-workflow >> - << pipeline.parameters.run-chat-workflow >> + - equal: [ << pipeline.trigger_source >>, scheduled_pipeline ] jobs: - noop - build-chat-offline-installers: + schedule: + # only run when scheduled by CircleCI when: - or: - - << pipeline.parameters.run-all-workflows >> - - << pipeline.parameters.run-chat-workflow >> + equal: [ << pipeline.trigger_source >>, scheduled_pipeline ] jobs: - - hold: + - build-offline-chat-installer-macos: + context: gpt4all + - build-offline-chat-installer-windows: + context: gpt4all + - build-offline-chat-installer-windows-arm: + context: gpt4all + - build-offline-chat-installer-linux: + context: gpt4all + - sign-online-chat-installer-macos: + context: gpt4all + requires: + - build-online-chat-installer-macos + - notarize-online-chat-installer-macos: + context: gpt4all + requires: + - sign-online-chat-installer-macos + - sign-online-chat-installer-windows: + context: gpt4all + requires: + - build-online-chat-installer-windows + - sign-online-chat-installer-windows-arm: + context: gpt4all + requires: + - build-online-chat-installer-windows-arm + build-chat-installers-release: + # only run on main branch tags that start with 'v' and a digit + when: + and: + - matches: { pattern: '^v\d.*', value: << pipeline.git.tag >> } + - not: + equal: [ << pipeline.trigger_source >>, scheduled_pipeline ] + jobs: + - validate-commit-on-main: + <<: *job_allow_tags + - build-offline-chat-installer-macos: + <<: *job_allow_tags + context: gpt4all + requires: + - validate-commit-on-main + - build-offline-chat-installer-windows: + <<: *job_allow_tags + context: gpt4all + requires: + - validate-commit-on-main + - build-offline-chat-installer-windows-arm: + <<: *job_allow_tags + context: gpt4all + requires: + - validate-commit-on-main + - build-offline-chat-installer-linux: + <<: *job_allow_tags + context: gpt4all + requires: + - validate-commit-on-main + - sign-offline-chat-installer-macos: + <<: *job_allow_tags + context: gpt4all + requires: + - build-offline-chat-installer-macos + - notarize-offline-chat-installer-macos: + <<: *job_allow_tags + context: gpt4all + requires: + - sign-offline-chat-installer-macos + - sign-offline-chat-installer-windows: + <<: *job_allow_tags + context: gpt4all + requires: + - build-offline-chat-installer-windows + - sign-offline-chat-installer-windows-arm: + <<: *job_allow_tags + context: gpt4all + requires: + - build-offline-chat-installer-windows-arm + - build-online-chat-installer-macos: + <<: *job_allow_tags + context: gpt4all + requires: + - validate-commit-on-main + - build-online-chat-installer-windows: + <<: *job_allow_tags + context: gpt4all + requires: + - validate-commit-on-main + - build-online-chat-installer-windows-arm: + <<: *job_allow_tags + context: gpt4all + requires: + - validate-commit-on-main + - build-online-chat-installer-linux: + <<: *job_allow_tags + context: gpt4all + requires: + - validate-commit-on-main + - sign-online-chat-installer-macos: + <<: *job_allow_tags + context: gpt4all + requires: + - build-online-chat-installer-macos + - notarize-online-chat-installer-macos: + <<: *job_allow_tags + context: gpt4all + requires: + - sign-online-chat-installer-macos + - sign-online-chat-installer-windows: + <<: *job_allow_tags + context: gpt4all + requires: + - build-online-chat-installer-windows + - sign-online-chat-installer-windows-arm: + <<: *job_allow_tags + context: gpt4all + requires: + - build-online-chat-installer-windows-arm + build-chat-offline-installers: + <<: *workflow-when-chat-requested + jobs: + - build-hold: + type: approval + - sign-hold: type: approval - build-offline-chat-installer-macos: + context: gpt4all requires: - - hold + - build-hold - sign-offline-chat-installer-macos: + context: gpt4all requires: + - sign-hold - build-offline-chat-installer-macos - notarize-offline-chat-installer-macos: + context: gpt4all requires: - sign-offline-chat-installer-macos - build-offline-chat-installer-windows: + context: gpt4all requires: - - hold + - build-hold - sign-offline-chat-installer-windows: + context: gpt4all requires: + - sign-hold - build-offline-chat-installer-windows + - build-offline-chat-installer-windows-arm: + context: gpt4all + requires: + - build-hold + - sign-offline-chat-installer-windows-arm: + context: gpt4all + requires: + - sign-hold + - build-offline-chat-installer-windows-arm - build-offline-chat-installer-linux: + context: gpt4all requires: - - hold + - build-hold build-chat-online-installers: - when: - or: - - << pipeline.parameters.run-all-workflows >> - - << pipeline.parameters.run-chat-workflow >> + <<: *workflow-when-chat-requested jobs: - - hold: + - build-hold: + type: approval + - sign-hold: type: approval - build-online-chat-installer-macos: + context: gpt4all requires: - - hold + - build-hold - sign-online-chat-installer-macos: + context: gpt4all requires: + - sign-hold - build-online-chat-installer-macos - notarize-online-chat-installer-macos: + context: gpt4all requires: - sign-online-chat-installer-macos - build-online-chat-installer-windows: + context: gpt4all requires: - - hold + - build-hold - sign-online-chat-installer-windows: + context: gpt4all requires: + - sign-hold - build-online-chat-installer-windows + - build-online-chat-installer-windows-arm: + context: gpt4all + requires: + - build-hold + - sign-online-chat-installer-windows-arm: + context: gpt4all + requires: + - sign-hold + - build-online-chat-installer-windows-arm - build-online-chat-installer-linux: + context: gpt4all requires: - - hold + - build-hold build-and-test-gpt4all-chat: - when: - or: - - << pipeline.parameters.run-all-workflows >> - - << pipeline.parameters.run-chat-workflow >> + <<: *workflow-when-chat-requested jobs: - hold: type: approval - build-gpt4all-chat-linux: + context: gpt4all requires: - hold - build-gpt4all-chat-windows: + context: gpt4all requires: - hold - build-gpt4all-chat-macos: + context: gpt4all requires: - hold deploy-docs: when: - or: - - << pipeline.parameters.run-all-workflows >> - - << pipeline.parameters.run-python-workflow >> + and: + - equal: [ << pipeline.git.branch >>, main ] + - or: + - << pipeline.parameters.run-all-workflows >> + - << pipeline.parameters.run-python-workflow >> + - not: + equal: [ << pipeline.trigger_source >>, scheduled_pipeline ] jobs: - - build-ts-docs: - filters: - branches: - only: - - main - - build-py-docs: - filters: - branches: - only: - - main - build-py-deploy: + - deploy-docs: + context: gpt4all + build-python: when: - or: - - << pipeline.parameters.run-all-workflows >> - - << pipeline.parameters.run-python-workflow >> + and: + - or: [ << pipeline.parameters.run-all-workflows >>, << pipeline.parameters.run-python-workflow >> ] + - not: + equal: [ << pipeline.trigger_source >>, scheduled_pipeline ] jobs: - pypi-hold: + <<: *job_only_main type: approval - hold: type: approval - build-py-linux: - filters: - branches: - only: requires: - hold - build-py-macos: - filters: - branches: - only: requires: - hold - build-py-windows: - filters: - branches: - only: requires: - hold - - store-and-upload-wheels: - filters: - branches: - only: + - deploy-wheels: + <<: *job_only_main + context: gpt4all requires: - pypi-hold - build-py-windows @@ -1598,66 +2153,48 @@ workflows: - build-py-macos build-bindings: when: - or: - - << pipeline.parameters.run-all-workflows >> - - << pipeline.parameters.run-python-workflow >> - - << pipeline.parameters.run-ts-workflow >> + and: + - or: [ << pipeline.parameters.run-all-workflows >>, << pipeline.parameters.run-ts-workflow >> ] + - not: + equal: [ << pipeline.trigger_source >>, scheduled_pipeline ] jobs: - - hold: - type: approval - - nuget-hold: + - backend-hold: type: approval - nodejs-hold: type: approval - npm-hold: + <<: *job_only_main + type: approval + - docs-hold: type: approval - build-bindings-backend-linux: - filters: - branches: - only: requires: - - hold + - backend-hold - build-bindings-backend-macos: - filters: - branches: - only: requires: - - hold + - backend-hold - build-bindings-backend-windows: - filters: - branches: - only: requires: - - hold - - # NodeJs Jobs - - prepare-npm-pkg: - filters: - branches: - only: - requires: - - npm-hold - - build-nodejs-linux - - build-nodejs-windows - - build-nodejs-macos + - backend-hold - build-nodejs-linux: - filters: - branches: - only: requires: - nodejs-hold - build-bindings-backend-linux - build-nodejs-windows: - filters: - branches: - only: requires: - nodejs-hold - build-bindings-backend-windows - build-nodejs-macos: - filters: - branches: - only: requires: - nodejs-hold - build-bindings-backend-macos + - build-ts-docs: + requires: + - docs-hold + - deploy-npm-pkg: + <<: *job_only_main + requires: + - npm-hold + - build-nodejs-linux + - build-nodejs-windows + - build-nodejs-macos diff --git a/.gitmodules b/.gitmodules index 752b837abd51..82388e151a69 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,9 @@ [submodule "gpt4all-chat/deps/QXlsx"] path = gpt4all-chat/deps/QXlsx url = https://github.com/nomic-ai/QXlsx.git +[submodule "gpt4all-chat/deps/minja"] + path = gpt4all-chat/deps/minja + url = https://github.com/nomic-ai/minja.git +[submodule "gpt4all-chat/deps/json"] + path = gpt4all-chat/deps/json + url = https://github.com/nlohmann/json.git diff --git a/MAINTAINERS.md b/MAINTAINERS.md index cbd69fc962ac..d307c0084ced 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -51,11 +51,6 @@ Thiago Ramos ([@thiagojramos](https://github.com/thiagojramos))
E-mail: thiagojramos@outlook.com
- pt\_BR translation -Victor Emanuel ([@SINAPSA-IC](https://github.com/SINAPSA-IC))
-E-mail: contact@sinapsaro.ro
-Discord: `@sinapsa_ic_56124_99632` -- ro\_RO translation - 不知火 Shiranui ([@supersonictw](https://github.com/supersonictw))
E-mail: supersonic@livemail.tw
Discord: `@supersonictw` diff --git a/README.md b/README.md index 5bb71f34c482..9d2575c1a6b0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@

GPT4All

+

+ Now with support for DeepSeek R1 Distillations +

+

WebsiteDocumentationDiscordYouTube Tutorial

@@ -23,9 +27,6 @@ https://github.com/nomic-ai/gpt4all/assets/70534565/513a0f15-4964-4109-89e4-4f9a

GPT4All is made possible by our compute partner Paperspace.

-

- phorm.ai -

## Download Links diff --git a/common/common.cmake b/common/common.cmake index a3d3b1a0c005..b8b6e969a357 100644 --- a/common/common.cmake +++ b/common/common.cmake @@ -11,7 +11,6 @@ function(gpt4all_add_warning_options target) -Wextra-semi -Wformat=2 -Wmissing-include-dirs - -Wstrict-overflow=2 -Wsuggest-override -Wvla # errors diff --git a/gpt4all-backend/deps/llama.cpp-mainline b/gpt4all-backend/deps/llama.cpp-mainline index 58a55efc4ae5..b06658d366ab 160000 --- a/gpt4all-backend/deps/llama.cpp-mainline +++ b/gpt4all-backend/deps/llama.cpp-mainline @@ -1 +1 @@ -Subproject commit 58a55efc4ae5dd3bc12887d47981faa7136027af +Subproject commit b06658d366abe3cea92f4e868db72165531a74da diff --git a/gpt4all-backend/include/gpt4all-backend/llmodel.h b/gpt4all-backend/include/gpt4all-backend/llmodel.h index ed5c4878fc6c..8695a5b5d832 100644 --- a/gpt4all-backend/include/gpt4all-backend/llmodel.h +++ b/gpt4all-backend/include/gpt4all-backend/llmodel.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,10 @@ using namespace std::string_literals; class LLModel { public: using Token = int32_t; + using PromptCallback = std::function batch, bool cached)>; + using ResponseCallback = std::function; + using EmbedCancelCallback = bool(unsigned *batchSizes, unsigned nBatch, const char *backend); + using ProgressCallback = std::function; class BadArchError: public std::runtime_error { public: @@ -101,6 +106,7 @@ class LLModel { static int32_t maxContextLength(const std::string &modelPath); static int32_t layerCount(const std::string &modelPath); static bool isEmbeddingModel(const std::string &modelPath); + static auto chatTemplate(const char *modelPath) -> std::expected; static void setImplementationsSearchPath(const std::string &path); static const std::string &implementationsSearchPath(); static bool hasSupportedCPU(); @@ -124,7 +130,6 @@ class LLModel { }; struct PromptContext { - int32_t n_past = 0; // number of tokens in past conversation int32_t n_predict = 200; int32_t top_k = 40; float top_p = 0.9f; @@ -136,8 +141,6 @@ class LLModel { float contextErase = 0.5f; // percent of context to erase if we exceed the context window }; - using ProgressCallback = std::function; - explicit LLModel() {} virtual ~LLModel() {} @@ -154,16 +157,12 @@ class LLModel { // This method requires the model to return true from supportsCompletion otherwise it will throw // an error - virtual void prompt(const std::string &prompt, - const std::string &promptTemplate, - std::function promptCallback, - std::function responseCallback, - bool allowContextShift, - PromptContext &ctx, - bool special = false, - std::optional fakeReply = {}); + virtual void prompt(std::string_view prompt, + const PromptCallback &promptCallback, + const ResponseCallback &responseCallback, + const PromptContext &ctx); - using EmbedCancelCallback = bool(unsigned *batchSizes, unsigned nBatch, const char *backend); + virtual int32_t countPromptTokens(std::string_view prompt) const; virtual size_t embeddingSize() const { throw std::logic_error(std::string(implementation().modelType()) + " does not support embeddings"); @@ -209,23 +208,22 @@ class LLModel { void setProgressCallback(ProgressCallback callback) { m_progressCallback = callback; } virtual int32_t contextLength() const = 0; + virtual auto specialTokens() -> std::unordered_map const = 0; protected: // These are pure virtual because subclasses need to implement as the default implementation of // 'prompt' above calls these functions - virtual std::vector tokenize(std::string_view str, bool special = false) = 0; + virtual std::vector tokenize(std::string_view str) const = 0; virtual bool isSpecialToken(Token id) const = 0; virtual std::string tokenToString(Token id) const = 0; - virtual void initSampler(PromptContext &ctx) = 0; + virtual void initSampler(const PromptContext &ctx) = 0; virtual Token sampleToken() const = 0; - virtual bool evalTokens(PromptContext &ctx, std::span tokens) const = 0; - virtual void shiftContext(PromptContext &promptCtx) = 0; + virtual bool evalTokens(int32_t nPast, std::span tokens) const = 0; + virtual void shiftContext(const PromptContext &promptCtx, int32_t *nPast) = 0; virtual int32_t inputLength() const = 0; - virtual void setTokenizeInputPosition(int32_t pos) = 0; - virtual auto computeModelInputPosition(PromptContext &ctx, const std::vector &input) - -> std::vector::const_iterator = 0; - virtual void setModelInputPosition(PromptContext &ctx, int32_t pos) = 0; - virtual void appendInputToken(PromptContext &ctx, Token tok) = 0; + virtual int32_t computeModelInputPosition(std::span input) const = 0; + virtual void setModelInputPosition(int32_t pos) = 0; + virtual void appendInputToken(Token tok) = 0; virtual std::span inputTokens() const = 0; virtual const std::vector &endTokens() const = 0; virtual bool shouldAddBOS() const = 0; @@ -242,6 +240,12 @@ class LLModel { return -1; } + virtual auto chatTemplate(const char *modelPath) const -> std::expected + { + (void)modelPath; + return std::unexpected("not implemented"); + } + const Implementation *m_implementation = nullptr; ProgressCallback m_progressCallback; @@ -253,19 +257,15 @@ class LLModel { return true; } - bool decodePrompt(std::function promptCallback, - std::function responseCallback, - bool allowContextShift, - PromptContext &promptCtx, - std::vector embd_inp, - bool isResponse = false, - bool alwaysDecode = false); - void generateResponse(std::function responseCallback, - bool allowContextShift, - PromptContext &promptCtx); - -protected: - Token m_tokenize_last_token = -1; // not serialized + // prefill context with prompt + auto decodePrompt(const PromptCallback &promptCallback, + const PromptContext &promptCtx, + std::vector embd_inp) + -> std::optional; + // generate a response + void generateResponse(const ResponseCallback &responseCallback, + const PromptContext &promptCtx, + int32_t nPast); friend class LLMImplementation; }; diff --git a/gpt4all-backend/include/gpt4all-backend/llmodel_c.h b/gpt4all-backend/include/gpt4all-backend/llmodel_c.h index e9497d0fafd9..271475bae480 100644 --- a/gpt4all-backend/include/gpt4all-backend/llmodel_c.h +++ b/gpt4all-backend/include/gpt4all-backend/llmodel_c.h @@ -35,16 +35,15 @@ typedef int32_t token_t; * behavior. */ struct llmodel_prompt_context { - int32_t n_past; // number of tokens in past conversation int32_t n_predict; // number of tokens to predict int32_t top_k; // top k logits to sample from - float top_p; // nucleus sampling probability threshold - float min_p; // Min P sampling - float temp; // temperature to adjust model's output distribution + float top_p; // nucleus sampling probability threshold + float min_p; // Min P sampling + float temp; // temperature to adjust model's output distribution int32_t n_batch; // number of predictions to generate in parallel - float repeat_penalty; // penalty factor for repeated tokens + float repeat_penalty; // penalty factor for repeated tokens int32_t repeat_last_n; // last n tokens to penalize - float context_erase; // percent of context to erase if we exceed the context window + float context_erase; // percent of context to erase if we exceed the context window }; struct llmodel_gpu_device { @@ -63,10 +62,12 @@ typedef struct llmodel_gpu_device llmodel_gpu_device; /** * Callback type for prompt processing. - * @param token_id The token id of the prompt. + * @param token_ids An array of token ids of the prompt. + * @param n_token_ids The number of tokens in the array. + * @param cached Whether the tokens were already in cache. * @return a bool indicating whether the model should keep processing. */ -typedef bool (*llmodel_prompt_callback)(int32_t token_id); +typedef bool (*llmodel_prompt_callback)(const token_t *token_ids, size_t n_token_ids, bool cached); /** * Callback type for response. @@ -74,7 +75,7 @@ typedef bool (*llmodel_prompt_callback)(int32_t token_id); * @param response The response string. NOTE: a token_id of -1 indicates the string is an error string. * @return a bool indicating whether the model should keep generating. */ -typedef bool (*llmodel_response_callback)(int32_t token_id, const char *response); +typedef bool (*llmodel_response_callback)(token_t token_id, const char *response); /** * Embedding cancellation callback for use with llmodel_embed. @@ -85,6 +86,8 @@ typedef bool (*llmodel_response_callback)(int32_t token_id, const char *response */ typedef bool (*llmodel_emb_cancel_callback)(unsigned *batch_sizes, unsigned n_batch, const char *backend); +typedef void (*llmodel_special_token_callback)(const char *name, const char *token); + /** * Create a llmodel instance. * Recognises correct model type from file at model_path @@ -183,22 +186,17 @@ uint64_t llmodel_state_set_data(llmodel_model model, const uint8_t *state, uint6 * Generate a response using the model. * @param model A pointer to the llmodel_model instance. * @param prompt A string representing the input prompt. - * @param prompt_template A string representing the input prompt template. * @param prompt_callback A callback function for handling the processing of prompt. * @param response_callback A callback function for handling the generated response. - * @param allow_context_shift Whether to allow shifting of context to make room for more input. - * @param special True if special tokens in the prompt should be processed, false otherwise. - * @param fake_reply A string to insert into context as the model's reply, or NULL to generate one. * @param ctx A pointer to the llmodel_prompt_context structure. + * @param error A pointer to a string; will only be set on error. */ -void llmodel_prompt(llmodel_model model, const char *prompt, - const char *prompt_template, - llmodel_prompt_callback prompt_callback, - llmodel_response_callback response_callback, - bool allow_context_shift, - llmodel_prompt_context *ctx, - bool special, - const char *fake_reply); +bool llmodel_prompt(llmodel_model model, + const char *prompt, + llmodel_prompt_callback prompt_callback, + llmodel_response_callback response_callback, + llmodel_prompt_context *ctx, + const char **error); /** * Generate an embedding using the model. @@ -310,6 +308,10 @@ const char *llmodel_model_backend_name(llmodel_model model); */ const char *llmodel_model_gpu_device_name(llmodel_model model); +int32_t llmodel_count_prompt_tokens(llmodel_model model, const char *prompt, const char **error); + +void llmodel_model_foreach_special_token(llmodel_model model, llmodel_special_token_callback callback); + #ifdef __cplusplus } #endif diff --git a/gpt4all-backend/src/llamamodel.cpp b/gpt4all-backend/src/llamamodel.cpp index 453dbd972bd8..86c2ea1f93bb 100644 --- a/gpt4all-backend/src/llamamodel.cpp +++ b/gpt4all-backend/src/llamamodel.cpp @@ -202,7 +202,7 @@ static int32_t get_arch_key_u32(std::string const &modelPath, std::string const if (keyidx != -1) { value = gguf_get_val_u32(ctx, keyidx); } else { - std::cerr << __func__ << ": " << key << "not found in " << modelPath << "\n"; + std::cerr << __func__ << ": " << key << " not found in " << modelPath << "\n"; } } @@ -518,18 +518,13 @@ size_t LLamaModel::restoreState(std::span state, std::span LLamaModel::tokenize(std::string_view str, bool special) +std::vector LLamaModel::tokenize(std::string_view str) const { - bool atStart = m_tokenize_last_token == -1; - bool insertSpace = atStart || isSpecialToken(m_tokenize_last_token); std::vector fres(str.length() + 4); - int32_t fres_len = llama_tokenize_gpt4all( - d_ptr->model, str.data(), str.length(), fres.data(), fres.size(), /*add_special*/ atStart, - /*parse_special*/ special, /*insert_space*/ insertSpace + int32_t fres_len = llama_tokenize( + d_ptr->model, str.data(), str.length(), fres.data(), fres.size(), /*add_special*/ true, /*parse_special*/ true ); fres.resize(fres_len); - if (fres_len) - m_tokenize_last_token = fres.back(); return fres; } @@ -555,7 +550,7 @@ std::string LLamaModel::tokenToString(Token id) const return std::string(result.data(), result.size()); } -void LLamaModel::initSampler(PromptContext &promptCtx) +void LLamaModel::initSampler(const PromptContext &promptCtx) { auto *model = d_ptr->model; auto *chain = d_ptr->sampler_chain; @@ -589,7 +584,8 @@ void LLamaModel::initSampler(PromptContext &promptCtx) llama_sampler_init_top_p(promptCtx.top_p, 1), llama_sampler_init_min_p(promptCtx.min_p, 1), llama_sampler_init_temp(promptCtx.temp), - llama_sampler_init_dist(LLAMA_DEFAULT_SEED) + llama_sampler_init_softmax(), + llama_sampler_init_dist(LLAMA_DEFAULT_SEED), }; for (auto *smpl : samplers) llama_sampler_chain_add(chain, smpl); @@ -601,9 +597,11 @@ LLModel::Token LLamaModel::sampleToken() const return llama_sampler_sample(d_ptr->sampler_chain, d_ptr->ctx, -1); } -bool LLamaModel::evalTokens(PromptContext &ctx, std::span tokens) const +bool LLamaModel::evalTokens(int32_t nPast, std::span tokens) const { - llama_kv_cache_seq_rm(d_ptr->ctx, 0, ctx.n_past, -1); + assert(!tokens.empty()); + + llama_kv_cache_seq_rm(d_ptr->ctx, 0, nPast, -1); llama_batch batch = llama_batch_init(tokens.size(), 0, 1); @@ -611,7 +609,7 @@ bool LLamaModel::evalTokens(PromptContext &ctx, std::span tokens) c for (int32_t i = 0; i < batch.n_tokens; i++) { batch.token [i] = tokens[i]; - batch.pos [i] = ctx.n_past + i; + batch.pos [i] = nPast + i; batch.n_seq_id[i] = 1; batch.seq_id [i][0] = 0; batch.logits [i] = false; @@ -625,13 +623,13 @@ bool LLamaModel::evalTokens(PromptContext &ctx, std::span tokens) c return res == 0; } -void LLamaModel::shiftContext(PromptContext &promptCtx) +void LLamaModel::shiftContext(const PromptContext &promptCtx, int32_t *nPast) { // infinite text generation via context shifting // erase up to n_ctx*contextErase tokens int n_keep = shouldAddBOS(); - int n_past = promptCtx.n_past; + int n_past = *nPast; int n_discard = std::min(n_past - n_keep, int(contextLength() * promptCtx.contextErase)); assert(n_discard > 0); @@ -647,7 +645,7 @@ void LLamaModel::shiftContext(PromptContext &promptCtx) auto &inp = d_ptr->inputTokens; inp.erase(inp.begin() + n_keep, inp.begin() + n_keep + n_discard); - promptCtx.n_past = inp.size(); + *nPast = inp.size(); } int32_t LLamaModel::contextLength() const @@ -655,39 +653,37 @@ int32_t LLamaModel::contextLength() const return llama_n_ctx(d_ptr->ctx); } -int32_t LLamaModel::inputLength() const +auto LLamaModel::specialTokens() -> std::unordered_map const { - return d_ptr->inputTokens.size(); + if (!d_ptr->model) + throw std::logic_error("model not loaded"); + + std::unordered_map tokens; + if (auto id = llama_token_bos(d_ptr->model); id != LLAMA_TOKEN_NULL) + tokens.emplace("bos_token", tokenToString(id)); + if (auto id = llama_token_eos(d_ptr->model); id != LLAMA_TOKEN_NULL) + tokens.emplace("eos_token", tokenToString(id)); + return tokens; } -void LLamaModel::setTokenizeInputPosition(int32_t pos) +int32_t LLamaModel::inputLength() const { - assert(pos >= 0); - m_tokenize_last_token = pos ? d_ptr->inputTokens.at(size_t(pos) - 1) : -1; // not serialized + return d_ptr->inputTokens.size(); } -auto LLamaModel::computeModelInputPosition(PromptContext &ctx, const std::vector &input) - -> std::vector::const_iterator +int32_t LLamaModel::computeModelInputPosition(std::span input) const { - assert(ctx.n_past >= 0); - auto pos = size_t(ctx.n_past); - if (pos > d_ptr->inputTokens.size()) { - std::ostringstream ss; - ss << "n_past=" << pos << " is past end of token cache length=" << d_ptr->inputTokens.size(); - throw std::out_of_range(ss.str()); - } - // find common prefix auto cacheIt = d_ptr->inputTokens.begin(); auto inputIt = input.begin(); while (cacheIt < d_ptr->inputTokens.end() && inputIt < input.end() && *cacheIt == *inputIt) { - ++cacheIt; ++inputIt; ++pos; + ++cacheIt; ++inputIt; } // tell the caller to ignore the tokens between [begin, inputIt) - return inputIt; + return inputIt - input.begin(); } -void LLamaModel::setModelInputPosition(PromptContext &ctx, int32_t pos) +void LLamaModel::setModelInputPosition(int32_t pos) { auto &inp = d_ptr->inputTokens; assert(pos >= 0); @@ -695,13 +691,11 @@ void LLamaModel::setModelInputPosition(PromptContext &ctx, int32_t pos) // truncate token cache to end at the new n_past if (pos < inp.size()) inp.resize(pos); - ctx.n_past = pos; } -void LLamaModel::appendInputToken(PromptContext &ctx, Token tok) +void LLamaModel::appendInputToken(Token tok) { d_ptr->inputTokens.push_back(tok); - ctx.n_past += 1; } auto LLamaModel::inputTokens() const -> std::span @@ -729,6 +723,37 @@ int32_t LLamaModel::layerCount(std::string const &modelPath) const return get_arch_key_u32(modelPath, "block_count"); } +// TODO(jared): reduce redundant code and operations by combining all metadata getters for unloaded +// models into a class that keeps the model file open +auto LLamaModel::chatTemplate(const char *modelPath) const -> std::expected +{ + auto *ctx = load_gguf(modelPath); + if (!ctx) + return std::unexpected("failed to open model file"); + + std::expected result; + enum gguf_type ktype; + const int kid = gguf_find_key(ctx, "tokenizer.chat_template"); + if (kid == -1) { + result = std::unexpected("key not found"); + goto cleanup; + } + + ktype = gguf_get_kv_type(ctx, kid); + if (ktype != GGUF_TYPE_STRING) { + result = std::unexpected( + "expected key type STRING (" + std::to_string(GGUF_TYPE_STRING) + "), got " + std::to_string(ktype) + ); + goto cleanup; + } + + result = gguf_get_val_str(ctx, kid); + +cleanup: + gguf_free(ctx); + return result; +} + #ifdef GGML_USE_VULKAN static const char *getVulkanVendorName(uint32_t vendorID) { diff --git a/gpt4all-backend/src/llamamodel_impl.h b/gpt4all-backend/src/llamamodel_impl.h index d6290a061316..7d018ddb1083 100644 --- a/gpt4all-backend/src/llamamodel_impl.h +++ b/gpt4all-backend/src/llamamodel_impl.h @@ -11,6 +11,7 @@ #include #include #include +#include struct LLamaPrivate; struct EmbModelSpec; @@ -49,26 +50,26 @@ class LLamaModel : public LLModel { size_t *tokenCount = nullptr, bool doMean = true, bool atlas = false) override; int32_t contextLength() const override; + auto specialTokens() -> std::unordered_map const override; protected: - std::vector tokenize(std::string_view str, bool special) override; + std::vector tokenize(std::string_view str) const override; bool isSpecialToken(Token id) const override; std::string tokenToString(Token id) const override; - void initSampler(PromptContext &ctx) override; + void initSampler(const PromptContext &ctx) override; Token sampleToken() const override; - bool evalTokens(PromptContext &ctx, std::span tokens) const override; - void shiftContext(PromptContext &promptCtx) override; + bool evalTokens(int32_t nPast, std::span tokens) const override; + void shiftContext(const PromptContext &promptCtx, int32_t *nPast) override; int32_t inputLength() const override; - void setTokenizeInputPosition(int32_t pos) override; - auto computeModelInputPosition(PromptContext &ctx, const std::vector &input) - -> std::vector::const_iterator override; - void setModelInputPosition(PromptContext &ctx, int32_t pos) override; - void appendInputToken(PromptContext &ctx, Token tok) override; + int32_t computeModelInputPosition(std::span input) const override; + void setModelInputPosition(int32_t pos) override; + void appendInputToken(Token tok) override; std::span inputTokens() const override; const std::vector &endTokens() const override; bool shouldAddBOS() const override; int32_t maxContextLength(std::string const &modelPath) const override; int32_t layerCount(std::string const &modelPath) const override; + auto chatTemplate(const char *modelPath) const -> std::expected override; void embedInternal(const std::vector &texts, float *embeddings, std::string prefix, int dimensionality, size_t *tokenCount, bool doMean, bool atlas, EmbedCancelCallback *cancelCb, diff --git a/gpt4all-backend/src/llmodel.cpp b/gpt4all-backend/src/llmodel.cpp index 1acf0642ef2a..ee247f35c9d2 100644 --- a/gpt4all-backend/src/llmodel.cpp +++ b/gpt4all-backend/src/llmodel.cpp @@ -326,6 +326,12 @@ bool LLModel::Implementation::isEmbeddingModel(const std::string &modelPath) return llama && llama->isEmbeddingModel(modelPath); } +auto LLModel::Implementation::chatTemplate(const char *modelPath) -> std::expected +{ + auto *llama = constructGlobalLlama(); + return llama ? llama->chatTemplate(modelPath) : std::unexpected("backend not available"); +} + void LLModel::Implementation::setImplementationsSearchPath(const std::string& path) { s_implementations_search_path = path; diff --git a/gpt4all-backend/src/llmodel_c.cpp b/gpt4all-backend/src/llmodel_c.cpp index 068052665f39..a8c5554da0d3 100644 --- a/gpt4all-backend/src/llmodel_c.cpp +++ b/gpt4all-backend/src/llmodel_c.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -22,7 +21,6 @@ static_assert(sizeof(token_t) == sizeof(LLModel::Token)); struct LLModelWrapper { LLModel *llModel = nullptr; - LLModel::PromptContext promptContext; ~LLModelWrapper() { delete llModel; } }; @@ -126,49 +124,44 @@ uint64_t llmodel_state_set_data(llmodel_model model, const uint8_t *state, uint6 return wrapper->llModel->restoreState({state, size_t(state_size)}, {input_tokens, size_t(n_input_tokens)}); } -void llmodel_prompt(llmodel_model model, const char *prompt, - const char *prompt_template, - llmodel_prompt_callback prompt_callback, - llmodel_response_callback response_callback, - bool allow_context_shift, - llmodel_prompt_context *ctx, - bool special, - const char *fake_reply) +bool llmodel_prompt(llmodel_model model, + const char *prompt, + llmodel_prompt_callback prompt_callback, + llmodel_response_callback response_callback, + llmodel_prompt_context *ctx, + const char **error) { auto *wrapper = static_cast(model); - auto response_func = [response_callback](int32_t token_id, const std::string &response) { - return response_callback(token_id, response.c_str()); + // Copy the C prompt context + LLModel::PromptContext promptContext { + .n_predict = ctx->n_predict, + .top_k = ctx->top_k, + .top_p = ctx->top_p, + .min_p = ctx->min_p, + .temp = ctx->temp, + .n_batch = ctx->n_batch, + .repeat_penalty = ctx->repeat_penalty, + .repeat_last_n = ctx->repeat_last_n, + .contextErase = ctx->context_erase, }; - // Copy the C prompt context - wrapper->promptContext.n_past = ctx->n_past; - wrapper->promptContext.n_predict = ctx->n_predict; - wrapper->promptContext.top_k = ctx->top_k; - wrapper->promptContext.top_p = ctx->top_p; - wrapper->promptContext.min_p = ctx->min_p; - wrapper->promptContext.temp = ctx->temp; - wrapper->promptContext.n_batch = ctx->n_batch; - wrapper->promptContext.repeat_penalty = ctx->repeat_penalty; - wrapper->promptContext.repeat_last_n = ctx->repeat_last_n; - wrapper->promptContext.contextErase = ctx->context_erase; + auto prompt_func = [prompt_callback](std::span token_ids, bool cached) { + return prompt_callback(token_ids.data(), token_ids.size(), cached); + }; + auto response_func = [response_callback](LLModel::Token token_id, std::string_view piece) { + return response_callback(token_id, piece.data()); + }; // Call the C++ prompt method - wrapper->llModel->prompt(prompt, prompt_template, prompt_callback, response_func, allow_context_shift, - wrapper->promptContext, special, - fake_reply ? std::make_optional(fake_reply) : std::nullopt); - - // Update the rest of the C prompt context - ctx->n_past = wrapper->promptContext.n_past; - ctx->n_predict = wrapper->promptContext.n_predict; - ctx->top_k = wrapper->promptContext.top_k; - ctx->top_p = wrapper->promptContext.top_p; - ctx->min_p = wrapper->promptContext.min_p; - ctx->temp = wrapper->promptContext.temp; - ctx->n_batch = wrapper->promptContext.n_batch; - ctx->repeat_penalty = wrapper->promptContext.repeat_penalty; - ctx->repeat_last_n = wrapper->promptContext.repeat_last_n; - ctx->context_erase = wrapper->promptContext.contextErase; + try { + wrapper->llModel->prompt(prompt, prompt_func, response_func, promptContext); + } catch (std::exception const &e) { + llmodel_set_error(error, e.what()); + return false; + } + + return true; } float *llmodel_embed( @@ -307,3 +300,21 @@ const char *llmodel_model_gpu_device_name(llmodel_model model) const auto *wrapper = static_cast(model); return wrapper->llModel->gpuDeviceName(); } + +int32_t llmodel_count_prompt_tokens(llmodel_model model, const char *prompt, const char **error) +{ + auto *wrapper = static_cast(model); + try { + return wrapper->llModel->countPromptTokens(prompt); + } catch (const std::exception& e) { + llmodel_set_error(error, e.what()); + return -1; + } +} + +void llmodel_model_foreach_special_token(llmodel_model model, llmodel_special_token_callback callback) +{ + auto *wrapper = static_cast(model); + for (auto &[name, token] : wrapper->llModel->specialTokens()) + callback(name.c_str(), token.c_str()); +} diff --git a/gpt4all-backend/src/llmodel_shared.cpp b/gpt4all-backend/src/llmodel_shared.cpp index ef046433a217..99782f44ca55 100644 --- a/gpt4all-backend/src/llmodel_shared.cpp +++ b/gpt4all-backend/src/llmodel_shared.cpp @@ -4,232 +4,120 @@ #include #include #include -#include #include #include #include -#include -#include +#include #include #include #include #include namespace ranges = std::ranges; +namespace views = std::ranges::views; -static bool parsePromptTemplate(const std::string &tmpl, std::vector &placeholders, std::string &err) -{ - static const std::regex placeholderRegex(R"(%[1-2](?![0-9]))"); +void LLModel::prompt( + std::string_view prompt, + const PromptCallback &promptCallback, + const ResponseCallback &responseCallback, + const PromptContext &promptCtx +) { + if (!isModelLoaded()) + throw std::invalid_argument("Attempted to prompt an unloaded model."); + if (!supportsCompletion()) + throw std::invalid_argument("Not a text completion model."); + if (!promptCtx.n_batch) + throw std::invalid_argument("Batch size cannot be zero."); + if (!promptCtx.n_predict) + return; // nothing requested - auto it = std::sregex_iterator(tmpl.begin(), tmpl.end(), placeholderRegex); - placeholders.clear(); - placeholders.insert(placeholders.end(), it, std::sregex_iterator()); + auto embd_inp = tokenize(prompt); + if (embd_inp.empty()) + throw std::invalid_argument("Prompt tokenized to zero tokens."); - if (placeholders.size() > 2) { - err = "ERROR: expected at most two placeholders, got " + std::to_string(placeholders.size()); - return false; - } - if (placeholders.size() >= 1 && placeholders[0].str() != "%1") { - err = "ERROR: first placeholder must be %1, got " + placeholders[0].str(); - return false; - } - if (placeholders.size() >= 2 && placeholders[1].str() != "%2") { - err = "ERROR: second placeholder must be %2, got " + placeholders[1].str(); - return false; - } - return true; + if (auto res = decodePrompt(promptCallback, promptCtx, std::move(embd_inp))) + generateResponse(responseCallback, promptCtx, /*n_past*/ *res); } -void LLModel::prompt(const std::string &prompt, - const std::string &promptTemplate, - std::function promptCallback, - std::function responseCallback, - bool allowContextShift, - PromptContext &promptCtx, - bool special, - std::optional fakeReply) +int32_t LLModel::countPromptTokens(std::string_view prompt) const { - if (!isModelLoaded()) { - std::cerr << implementation().modelType() << " ERROR: prompt won't work with an unloaded model!\n"; - return; - } - - if (!supportsCompletion()) { - std::string errorMessage = "ERROR: this model does not support text completion or chat!"; - responseCallback(-1, errorMessage); - std::cerr << implementation().modelType() << " " << errorMessage << "\n"; - return; - } + if (!isModelLoaded()) + throw std::invalid_argument("Attempted to tokenize with an unloaded model."); + return int32_t(tokenize(prompt).size()); +} - // sanity checks - if (promptCtx.n_past > contextLength()) { - std::ostringstream ss; - ss << "n_past=" << promptCtx.n_past << " is past end of context length=" << contextLength(); - throw std::out_of_range(ss.str()); - } - if (promptCtx.n_past > inputLength()) { - std::ostringstream ss; - ss << "n_past=" << promptCtx.n_past << " is past end of token cache length=" << inputLength(); - throw std::out_of_range(ss.str()); - } +auto LLModel::decodePrompt( + const PromptCallback &promptCallback, + const PromptContext &promptCtx, + std::vector embd_inp +) -> std::optional +{ + assert(!embd_inp.empty()); - promptCtx.n_batch = std::min(promptCtx.n_batch, LLMODEL_MAX_PROMPT_BATCH); + int32_t nCtx = contextLength(); + int32_t n_batch = std::min(promptCtx.n_batch, LLMODEL_MAX_PROMPT_BATCH); - // parse the prompt template - std::vector placeholders; - { - std::string err; - if (!parsePromptTemplate(promptTemplate, placeholders, err)) { - responseCallback(-1, err); - std::cerr << err << "\n"; - return; - } - } + // Find the greatest n_past where the beginning of embd_inp matches the end of the token cache, starting at the + // requested n_past. + // This is used to skip unnecessary work when the prompt shares a common prefix with the previous result. + int32_t nPast = computeModelInputPosition(embd_inp); - setTokenizeInputPosition(promptCtx.n_past); - - // tokenize the user prompt - std::vector embd_inp; - if (placeholders.empty()) { - // this is unusual, but well-defined - std::cerr << __func__ << ": prompt template has no placeholder\n"; - embd_inp = tokenize(promptTemplate, true); - } else { - // template: beginning of user prompt - const auto &phUser = placeholders[0]; - std::string userPrefix(phUser.prefix()); - if (!userPrefix.empty()) - embd_inp = tokenize(userPrefix, true); - - // user input (shouldn't have special token processing) - auto tokens = tokenize(prompt, special); - embd_inp.insert(embd_inp.end(), tokens.begin(), tokens.end()); - - // template: end of user prompt + start of assistant prompt - size_t start = phUser.position() + phUser.length(); - size_t end = placeholders.size() >= 2 ? placeholders[1].position() : promptTemplate.length(); - auto userToAsst = promptTemplate.substr(start, end - start); - if (!userToAsst.empty()) { - tokens = tokenize(userToAsst, true); - embd_inp.insert(embd_inp.end(), tokens.begin(), tokens.end()); - } - } + // always decode up to a full batch before generating, even if cached + nPast -= std::min(n_batch, nPast); - // decode the user prompt - if (!decodePrompt(promptCallback, responseCallback, allowContextShift, promptCtx, embd_inp, /*isResponse*/ false, - /*alwaysDecode*/ true)) - return; // error - - // decode the assistant's reply, either generated or spoofed - if (!fakeReply) { - generateResponse(responseCallback, allowContextShift, promptCtx); - } else { - embd_inp = tokenize(*fakeReply, false); - if (!decodePrompt(promptCallback, responseCallback, allowContextShift, promptCtx, embd_inp, true)) - return; // error - } + // TODO(jared): generalize this to find the smallest new_embd_inp.size() - nPast given the cache + if (!nPast && int32_t(embd_inp.size()) > nCtx) { + // no cache hit -> shift the input before even processing - // decode the rest of the prompt template - // template: end of assistant prompt - std::string asstSuffix; - if (placeholders.size() >= 2) { - size_t start = placeholders[1].position() + placeholders[1].length(); - asstSuffix = promptTemplate.substr(start); - } else { - asstSuffix = "\n\n"; // default to a blank link, good for e.g. Alpaca - } - if (!asstSuffix.empty()) { - embd_inp = tokenize(asstSuffix, true); - decodePrompt(promptCallback, responseCallback, allowContextShift, promptCtx, embd_inp); - } -} + int32_t nKeep = shouldAddBOS(); + auto newLength = int32_t(nCtx * (1.f - promptCtx.contextErase)); + int32_t nDiscard = int32_t(embd_inp.size()) - std::max(1, std::min(nCtx, newLength)); -// returns false on error -bool LLModel::decodePrompt(std::function promptCallback, - std::function responseCallback, - bool allowContextShift, - PromptContext &promptCtx, - std::vector embd_inp, - bool isResponse, - bool alwaysDecode) { - if ((int) embd_inp.size() > contextLength() - 4) { - // FIXME: (Adam) We should find a way to bubble these strings to the UI level to allow for - // translation - responseCallback(-1, "Your message was too long and could not be processed. Please try again with something shorter."); - std::cerr << implementation().modelType() << " ERROR: The prompt is " << embd_inp.size() << - " tokens and the context window is " << contextLength() << "!\n"; - return false; - } + // execute the callback even for skipped tokens. this misrepresents the position of BOS but we don't care + auto discardedTokens = embd_inp | views::drop(nKeep) | views::take(nDiscard); + if (!promptCallback(discardedTokens, true)) + return std::nullopt; - // FIXME(jared): There are mitigations for this situation, such as making room before - // copying the prompt context, or restoring the KV cache when we restore the prompt - // context. - if (!allowContextShift && promptCtx.n_past + embd_inp.size() > contextLength()) { - std::cerr << "LLModel Warning: Not enough space, n_past=" << promptCtx.n_past << ", n_eval=" << embd_inp.size() - << ", n_ctx=" << contextLength() << "\n"; - return false; - } + // erase nDiscard tokens + embd_inp.erase(discardedTokens.begin(), discardedTokens.end()); + assert(int32_t(embd_inp.size()) <= nCtx); - // always decode something before generating, even if cached - if (alwaysDecode && embd_inp.empty()) { - auto cache = inputTokens(); - if (!promptCtx.n_past) - throw std::runtime_error("zero token prompt is not supported"); - assert(!cache.empty()); - embd_inp.push_back(cache.back()); - promptCtx.n_past--; + // check the cache again, just in case + nPast = computeModelInputPosition(embd_inp); + nPast -= std::min(n_batch, nPast); } - // Find the greatest n_past where the beginning of embd_inp matches the end of the token cache, starting at the - // requested n_past. - // This is used to skip unnecessary work when the prompt shares a common prefix with the previous result. - auto embd_inp_start = computeModelInputPosition(promptCtx, embd_inp); - size_t start_offset = embd_inp_start - embd_inp.begin(); - - // always decode up to a full batch before generating, even if cached - if (alwaysDecode) - start_offset -= std::min(promptCtx.n_batch, int32_t(start_offset)); - - setModelInputPosition(promptCtx, promptCtx.n_past + start_offset); + setModelInputPosition(nPast); // execute the callback even for skipped tokens - size_t i = 0; - for (; i < start_offset; i++) { - Token tok = embd_inp[i]; - bool res = isResponse ? responseCallback(tok, tokenToString(tok)) : promptCallback(tok); - if (!res) - return false; - } + if (!promptCallback(embd_inp | views::take(nPast), true)) + return std::nullopt; // process the prompt in batches - while (i < embd_inp.size()) { - size_t batch_end = std::min(i + promptCtx.n_batch, embd_inp.size()); - std::span batch(embd_inp.begin() + i, embd_inp.begin() + batch_end); + for (int32_t i = nPast; i < embd_inp.size();) { + auto batch_end = std::min(i + n_batch, int32_t(embd_inp.size())); + std::span batch(embd_inp.begin() + i, embd_inp.begin() + batch_end); // Check if the context has run out... - if (promptCtx.n_past + int32_t(batch.size()) > contextLength()) { - assert(allowContextShift); - shiftContext(promptCtx); - assert(promptCtx.n_past + int32_t(batch.size()) <= contextLength()); + if (nPast + int32_t(batch.size()) > nCtx) { + shiftContext(promptCtx, &nPast); + assert(nPast + int32_t(batch.size()) <= nCtx); } - if (!evalTokens(promptCtx, batch)) { - std::cerr << implementation().modelType() << " ERROR: Failed to process prompt\n"; - return false; - } + // FIXME(Adam): We should find a way to bubble these strings to the UI level to allow for translation + if (!evalTokens(nPast, batch)) + throw std::runtime_error("An internal error was encountered during prompt processing."); - size_t tokens = batch_end - i; - for (size_t t = 0; t < tokens; ++t) { - Token tok = batch[t]; - appendInputToken(promptCtx, tok); - bool res = isResponse ? responseCallback(tok, tokenToString(tok)) : promptCallback(tok); - if (!res) - return false; + for (auto &tok : batch) { + appendInputToken(tok); + nPast++; + if (!promptCallback({ &tok, 1 }, false)) + return std::nullopt; } i = batch_end; } - return true; + return nPast; } /* @@ -251,22 +139,16 @@ static std::string::size_type stringsOverlap(const std::string &s, const std::st return std::string::npos; } -void LLModel::generateResponse(std::function responseCallback, - bool allowContextShift, - PromptContext &promptCtx) { +void LLModel::generateResponse( + const ResponseCallback &responseCallback, + const PromptContext &promptCtx, + int32_t nPast +) { static const char *stopSequences[] { - "### Instruction", "### Prompt", "### Response", "### Human", "### Assistant", "### Context", + "### System", "### Instruction", "### Human", "### User", "### Response", "### Assistant", "### Context", + "<|im_start|>", "<|im_end|>", "<|endoftext|>", }; - // Don't even start if there is no room - if (!promptCtx.n_predict) - return; - if (!allowContextShift && promptCtx.n_past >= contextLength()) { - std::cerr << "LLModel Warning: Not enough space, n_past=" << promptCtx.n_past << ", n_ctx=" << contextLength() - << "\n"; - return; - } - initSampler(promptCtx); std::string cachedResponse; @@ -281,25 +163,20 @@ void LLModel::generateResponse(std::function cachedTokens.push_back(new_tok.value()); cachedResponse += new_piece; - auto accept = [this, &promptCtx, &new_tok, allowContextShift]() -> bool { + auto accept = [this, &promptCtx, &new_tok, &nPast] { // Shift context if out of space - if (promptCtx.n_past >= contextLength()) { - (void)allowContextShift; - assert(allowContextShift); - shiftContext(promptCtx); - assert(promptCtx.n_past < contextLength()); + if (nPast >= contextLength()) { + shiftContext(promptCtx, &nPast); + assert(nPast < contextLength()); } // Accept the token Token tok = std::exchange(new_tok, std::nullopt).value(); - if (!evalTokens(promptCtx, { &tok, 1 })) { - // TODO(jared): raise an exception - std::cerr << implementation().modelType() << " ERROR: Failed to predict next token\n"; - return false; - } + if (!evalTokens(nPast, { &tok, 1 })) + throw std::runtime_error("An internal error was encountered during response generation."); - appendInputToken(promptCtx, tok); - return true; + appendInputToken(tok); + nPast++; }; // Check for EOS @@ -336,13 +213,6 @@ void LLModel::generateResponse(std::function lengthLimit = cachedResponse.size() - new_piece.size(); } - // Optionally stop if the context will run out - if (!allowContextShift && promptCtx.n_past + cachedTokens.size() >= contextLength()) { - std::cerr << "LLModel Warning: Not enough space, n_past=" << promptCtx.n_past << ", n_ctx=" - << contextLength() << "\n"; - stop = true; - } - // Empty the cache, up to the length limit std::string::size_type responseLength = 0; while (!cachedTokens.empty()) { @@ -359,8 +229,8 @@ void LLModel::generateResponse(std::function cachedResponse.erase(cachedResponse.begin(), cachedResponse.begin() + piece.size()); // Accept the token, if needed (not cached) - if (cachedTokens.empty() && new_tok && !accept()) - return; + if (cachedTokens.empty() && new_tok) + accept(); // Send the token if (!responseCallback(tok, piece) || ++n_predicted >= promptCtx.n_predict) { @@ -379,8 +249,8 @@ void LLModel::generateResponse(std::function assert(!cachedTokens.empty() && cachedTokens.back() == new_tok); if (stop) { cachedTokens.pop_back(); - } else if (!accept()) { - return; + } else { + accept(); } } } @@ -396,8 +266,6 @@ void LLModel::generateResponse(std::function auto discard_start = inp.end() - cachedTokens.size(); assert(std::equal(discard_start, inp.end(), cachedTokens.begin())); #endif - - promptCtx.n_past -= cachedTokens.size(); } void LLModel::embed( diff --git a/gpt4all-bindings/python/CHANGELOG.md b/gpt4all-bindings/python/CHANGELOG.md index ec3ce4686b11..1007a6accf99 100644 --- a/gpt4all-bindings/python/CHANGELOG.md +++ b/gpt4all-bindings/python/CHANGELOG.md @@ -9,11 +9,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added - Warn on Windows if the Microsoft Visual C++ runtime libraries are not found ([#2920](https://github.com/nomic-ai/gpt4all/pull/2920)) - Basic cache for faster prefill when the input shares a prefix with previous context ([#3073](https://github.com/nomic-ai/gpt4all/pull/3073)) +- Add ability to modify or replace the history of an active chat session ([#3147](https://github.com/nomic-ai/gpt4all/pull/3147)) ### Changed - Rebase llama.cpp on latest upstream as of September 26th ([#2998](https://github.com/nomic-ai/gpt4all/pull/2998)) - Change the error message when a message is too long ([#3004](https://github.com/nomic-ai/gpt4all/pull/3004)) - Fix CalledProcessError on Intel Macs since v2.8.0 ([#3045](https://github.com/nomic-ai/gpt4all/pull/3045)) +- Use Jinja for chat templates instead of per-message QString.arg-style templates ([#3147](https://github.com/nomic-ai/gpt4all/pull/3147)) ## [2.8.2] - 2024-08-14 diff --git a/gpt4all-bindings/python/docs/gpt4all_desktop/chat_templates.md b/gpt4all-bindings/python/docs/gpt4all_desktop/chat_templates.md new file mode 100644 index 000000000000..5c15cf620c85 --- /dev/null +++ b/gpt4all-bindings/python/docs/gpt4all_desktop/chat_templates.md @@ -0,0 +1,206 @@ +## What are chat templates? +Natively, large language models only know how to complete plain text and do not know the difference between their input and their output. In order to support a chat with a person, LLMs are designed to use a template to convert the conversation to plain text using a specific format. + +For a given model, it is important to use an appropriate chat template, as each model is designed to work best with a specific format. The chat templates included with the built-in models should be sufficient for most purposes. + +There are two reasons you would want to alter the chat template: + +- You are sideloading a model and there is no chat template available, +- You would like to have greater control over the input to the LLM than a system message provides. + + +## What is a system message? +A system message is a message that controls the responses from the LLM in a way that affects the entire conversation. System messages can be short, such as "Speak like a pirate.", or they can be long and contain a lot of context for the LLM to keep in mind. + +Not all models are designed to use a system message, so they work with some models better than others. + + +## How do I customize the chat template or system message? +To customize the chat template or system message, go to Settings > Model. Make sure to select the correct model at the top. If you clone a model, you can use a different chat template or system message from the base model, enabling you to use different settings for each conversation. + +These settings take effect immediately. After changing them, you can click "Redo last response" in the chat view, and the response will take the new settings into account. + + +## Do I need to write a chat template? +You typically do not need to write your own chat template. The exception is models that are not in the official model list and do not come with a chat template built-in. These will show a "Clear" option above the chat template field in the Model Settings page instead of a "Reset" option. See the section on [finding] or [creating] a chat template. + +[finding]: #how-do-i-find-a-chat-template +[creating]: #advanced-how-do-chat-templates-work + + +## What changed in GPT4All v3.5? +GPT4All v3.5 overhauled the chat template system. There are three crucial differences: + +- The chat template now formats an entire conversation instead of a single pair of messages, +- The chat template now uses Jinja syntax instead of `%1` and `%2` placeholders, +- And the system message should no longer contain control tokens or trailing whitespace. + +If you are using any chat templates or system messages that had been added or altered from the default before upgrading to GPT4All v3.5 or newer, these will no longer work. See below for how to solve common errors you may see after upgrading. + + +## Error/Warning: System message is not plain text. +This is easy to fix. Go to the model's settings and look at the system prompt. There are three things to look for: + +- Control tokens such as `<|im_start|>`, `<|start_header_id|>`, or `<|system|>` +- A prefix such as `### System` or `SYSTEM:` +- Trailing whitespace, such as a space character or blank line. + +If you see any of these things, remove them. For example, this legacy system prompt: +``` +<|start_header_id|>system<|end_header_id|> +You are a helpful assistant.<|eot_id|> +``` + +Should become this: +``` +You are a helpful assistant. +``` + +If you do not see anything that needs to be changed, you can dismiss the error by making a minor modification to the message and then changing it back. + +If you see a warning, your system message does not appear to be plain text. If you believe this warning is incorrect, it can be safely ignored. If in doubt, ask on the [Discord]. + +[Discord]: https://discord.gg/mGZE39AS3e + + +## Error: Legacy system prompt needs to be updated in Settings. +This is the same as [above][above-1], but appears on the chat page. + +[above-1]: #errorwarning-system-message-is-not-plain-text + + +## Error/Warning: Chat template is not in Jinja format. +This is the result of attempting to use an old-style template (possibly from a previous version) in GPT4All 3.5+. + +Go to the Model Settings page and select the affected model. If you see a "Reset" button, and you have not intentionally modified the prompt template, you can click "Reset". Otherwise, this is what you can do: + +1. Back up your chat template by copying it safely to a text file and saving it. In the next step, it will be removed from GPT4All. +2. Click "Reset" or "Clear". +3. If you clicked "Clear", the chat template is now gone. Follow the steps to [find][finding] or [create][creating] a basic chat template for your model. +4. Customize the chat template to suit your needs. For help, read the section about [creating] a chat template. + + +## Error: Legacy prompt template needs to be updated in Settings. +This is the same as [above][above-2], but appears on the chat page. + +[above-2]: #errorwarning-chat-template-is-not-in-jinja-format + + +## The chat template has a syntax error. +If there is a syntax error while editing the chat template, the details will be displayed in an error message above the input box. This could be because the chat template is not actually in Jinja format (see [above][above-2]). + +Otherwise, you have either typed something correctly, or the model comes with a template that is incompatible with GPT4All. See [the below section][creating] on creating chat templates and make sure that everything is correct. When in doubt, ask on the [Discord]. + + +## Error: No chat template configured. +This may appear for models that are not from the official model list and do not include a chat template. Older versions of GPT4All picked a poor default in this case. You will get much better results if you follow the steps to [find][finding] or [create][creating] a chat template for your model. + + +## Error: The chat template cannot be blank. +If the button above the chat template on the Model Settings page says "Clear", see [above][above-3]. If you see "Reset", click that button to restore a reasonable default. Also see the section on [syntax errors][chat-syntax-error]. + +[above-3]: #error-no-chat-template-configured +[chat-syntax-error]: #the-chat-template-has-a-syntax-error + + +## How do I find a chat template? +When in doubt, you can always ask the [Discord] community for help. Below are the instructions to find one on your own. + +The authoritative source for a model's chat template is the HuggingFace repo that the original (non-GGUF) model came from. First, you should find this page. If you just have a model file, you can try a google search for the model's name. If you know the page you downloaded the GGUF model from, its README usually links to the original non-GGUF model. + +Once you have located the original model, there are two methods you can use to extract its chat template. Pick whichever one you are most comfortable with. + +### Using the CLI (all models) +1. Install `jq` using your preferred package manager - e.g. Chocolatey (Windows), Homebrew (macOS), or apt (Ubuntu). +2. Download `tokenizer_config.json` from the model's "Files and versions" tab. +3. Open a command prompt in the directory which you have downloaded the model file. +4. Run `jq -r ".chat_template" tokenizer_config.json`. This shows the chat template in a human-readable form. You can copy this and paste it into the settings page. +5. (Optional) You can save the output to a text file like this: `jq -r ".chat_template" tokenizer_config.json >chat_template.txt` + +If the output is "null", the model does not provide a chat template. See the [below instructions][creating] on creating a chat template. + +### Python (open models) +1. Install `transformers` using your preferred python package manager, e.g. `pip install transformers`. Make sure it is at least version v4.43.0. +2. Copy the ID of the HuggingFace model, using the clipboard icon next to the name. For example, if the URL is `https://huggingface.co/NousResearch/Hermes-2-Pro-Llama-3-8B`, the ID is `NousResearch/Hermes-2-Pro-Llama-3-8B`. +3. Open a python interpreter (`python`) and run the following commands. Change the model ID in the example to the one you copied. +``` +>>> from transformers import AutoTokenizer +>>> tokenizer = AutoTokenizer.from_pretrained('NousResearch/Hermes-2-Pro-Llama-3-8B') +>>> print(tokenizer.get_chat_template()) +``` +You can copy the output and paste it into the settings page. +4. (Optional) You can save the output to a text file like this: +``` +>>> open('chat_template.txt', 'w').write(tokenizer.get_chat_template()) +``` + +If you get a ValueError exception, this model does not provide a chat template. See the [below instructions][creating] on creating a chat template. + + +### Python (gated models) +Some models, such as Llama and Mistral, do not allow public access to their chat template. You must either use the CLI method above, or follow the following instructions to use Python: + +1. For these steps, you must have git and git-lfs installed. +2. You must have a HuggingFace account and be logged in. +3. You must already have access to the gated model. Otherwise, request access. +4. You must have an SSH key configured for git access to HuggingFace. +5. `git clone` the model's HuggingFace repo using the SSH clone URL. There is no need to download the entire model, which is very large. A good way to do this on Linux is: +```console +$ GIT_LFS_SKIP_SMUDGE=1 git clone hf.co:meta-llama/Llama-3.1-8B-Instruct.git +$ cd Llama-3.1-8B-Instruct +$ git lfs pull -I "tokenizer.*" +``` +6. Follow the above instructions for open models, but replace the model ID with the path to the directory containing `tokenizer\_config.json`: +``` +>>> tokenizer = AutoTokenizer.from_pretrained('.') +``` + + +## Advanced: How do chat templates work? +The chat template is applied to the entire conversation you see in the chat window. The template loops over the list of messages, each containing `role` and `content` fields. `role` is either `user`, `assistant`, or `system`. + +GPT4All also supports the special variables `bos_token`, `eos_token`, and `add_generation_prompt`. See the [HuggingFace docs] for what those do. + +[HuggingFace docs]: https://huggingface.co/docs/transformers/v4.46.3/en/chat_templating#special-variables + + +## Advanced: How do I make a chat template? +The best way to create a chat template is to start by using an existing one as a reference. Then, modify it to use the format documented for the given model. Its README page may explicitly give an example of its template. Or, it may mention the name of a well-known standard template, such as ChatML, Alpaca, Vicuna. GPT4All does not yet include presets for these templates, so they will have to be found in other models or taken from the community. + +For more information, see the very helpful [HuggingFace guide]. Some of this is not applicable, such as the information about tool calling and RAG - GPT4All implements those features differently. + +Some models use a prompt template that does not intuitively map to a multi-turn chat, because it is more intended for single instructions. The [FastChat] implementation of these templates is a useful reference for the correct way to extend them to multiple messages. + +[HuggingFace guide]: https://huggingface.co/docs/transformers/v4.46.3/en/chat_templating#advanced-template-writing-tips +[FastChat]: https://github.com/lm-sys/FastChat/blob/main/fastchat/conversation.py + + +# Advanced: What are GPT4All v1 templates? +GPT4All supports its own template syntax, which is nonstandard but provides complete control over the way LocalDocs sources and file attachments are inserted into the conversation. These templates begin with `{# gpt4all v1 #}` and look similar to the example below. + +For standard templates, GPT4All combines the user message, sources, and attachments into the `content` field. For GPT4All v1 templates, this is not done, so they must be used directly in the template for those features to work correctly. + +```jinja +{# gpt4all v1 #} +{%- for message in messages %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' }} + {%- if message['role'] == 'user' %} + {%- for source in message['sources'] %} + {%- if loop.first %} + {{- '### Context:\n' }} + {%- endif %} + {{- 'Collection: ' + source['collection'] + '\n' + + 'Path: ' + source['path'] + '\n' + + 'Excerpt: ' + source['text'] + '\n\n' }} + {%- endfor %} + {%- endif %} + {%- for attachment in message['prompt_attachments'] %} + {{- attachment['processed_content'] + '\n\n' }} + {%- endfor %} + {{- message['content'] | trim }} + {{- '<|eot_id|>' }} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %} +``` diff --git a/gpt4all-bindings/python/docs/gpt4all_desktop/settings.md b/gpt4all-bindings/python/docs/gpt4all_desktop/settings.md index 87e8aeccf0de..e9d5eb8531ec 100644 --- a/gpt4all-bindings/python/docs/gpt4all_desktop/settings.md +++ b/gpt4all-bindings/python/docs/gpt4all_desktop/settings.md @@ -8,8 +8,10 @@ | --- | --- | --- | | **Theme** | Color theme for the application. Options are `Light`, `Dark`, and `LegacyDark` | `Light` | | **Font Size** | Font size setting for text throughout the application. Options are Small, Medium, and Large | Small | + | **Language and Locale** | The language and locale of that language you wish to use | System Locale | | **Device** | Device that will run your models. Options are `Auto` (GPT4All chooses), `Metal` (Apple Silicon M1+), `CPU`, and `GPU` | `Auto` | | **Default Model** | Choose your preferred LLM to load by default on startup| Auto | + | **Suggestion Mode** | Generate suggested follow up questions at the end of responses | When chatting with LocalDocs | | **Download Path** | Select a destination on your device to save downloaded models | Windows: `C:\Users\{username}\AppData\Local\nomic.ai\GPT4All`

Mac: `/Users/{username}/Library/Application Support/nomic.ai/GPT4All/`

Linux: `/home/{username}/.local/share/nomic.ai/GPT4All` | | **Enable Datalake** | Opt-in to sharing interactions with GPT4All community (**anonymous** and **optional**) | Off | @@ -18,7 +20,7 @@ | Setting | Description | Default Value | | --- | --- | --- | | **CPU Threads** | Number of concurrently running CPU threads (more can speed up responses) | 4 | - | **Save Chat Context** | Save chat context to disk to pick up exactly where a model left off. | Off | + | **Enable System Tray** | The application will minimize to the system tray / taskbar when the window is closed | Off | | **Enable Local Server** | Allow any application on your device to use GPT4All via an OpenAI-compatible GPT4All API | Off | | **API Server Port** | Local HTTP port for the local API server | 4891 | @@ -29,8 +31,11 @@ | Setting | Description | Default Value | | --- | --- | --- | | **Name** | Unique name of this model / character| set by model uploader | - | **System Prompt** | General instructions for the chats this model will be used for | set by model uploader | - | **Prompt Template** | Format of user <-> assistant interactions for the chats this model will be used for | set by model uploader | + | **Model File** | Filename (.gguf) of the model | set by model uploader | + | **System Message** | General instructions for the chats this model will be used for | set by model uploader | + | **Chat Template** | Format of user <-> assistant interactions for the chats this model will be used for | set by model uploader | + | **Chat Name Prompt** | Prompt used to automatically generate chat names | Describe the above conversation in seven words or less. | + | **Suggested FollowUp Prompt** | Prompt used to automatically generate follow up questions after a chat response | Suggest three very short factual follow-up questions that have not been answered yet or cannot be found inspired by the previous conversation and excerpts. | ### Clone diff --git a/gpt4all-bindings/python/gpt4all/_pyllmodel.py b/gpt4all-bindings/python/gpt4all/_pyllmodel.py index 136cf685aa2b..616ce80a3533 100644 --- a/gpt4all-bindings/python/gpt4all/_pyllmodel.py +++ b/gpt4all-bindings/python/gpt4all/_pyllmodel.py @@ -9,7 +9,7 @@ import threading from enum import Enum from queue import Queue -from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, Literal, NoReturn, TypeVar, overload +from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, Iterator, Literal, NoReturn, TypeVar, overload if sys.version_info >= (3, 9): import importlib.resources as importlib_resources @@ -23,7 +23,9 @@ from typing import TypedDict if TYPE_CHECKING: - from typing_extensions import TypeAlias + from typing_extensions import ParamSpec, TypeAlias + T = TypeVar("T") + P = ParamSpec("P") EmbeddingsType = TypeVar('EmbeddingsType', bound='list[Any]') @@ -31,7 +33,7 @@ # TODO(jared): use operator.call after we drop python 3.10 support -def _operator_call(obj, /, *args, **kwargs): +def _operator_call(obj: Callable[P, T], /, *args: P.args, **kwargs: P.kwargs) -> T: return obj(*args, **kwargs) @@ -116,16 +118,15 @@ def load_llmodel_library(): class LLModelPromptContext(ctypes.Structure): _fields_ = [ - ("n_past", ctypes.c_int32), - ("n_predict", ctypes.c_int32), - ("top_k", ctypes.c_int32), - ("top_p", ctypes.c_float), - ("min_p", ctypes.c_float), - ("temp", ctypes.c_float), - ("n_batch", ctypes.c_int32), + ("n_predict", ctypes.c_int32), + ("top_k", ctypes.c_int32), + ("top_p", ctypes.c_float), + ("min_p", ctypes.c_float), + ("temp", ctypes.c_float), + ("n_batch", ctypes.c_int32), ("repeat_penalty", ctypes.c_float), - ("repeat_last_n", ctypes.c_int32), - ("context_erase", ctypes.c_float), + ("repeat_last_n", ctypes.c_int32), + ("context_erase", ctypes.c_float), ] @@ -157,23 +158,21 @@ class LLModelGPUDevice(ctypes.Structure): llmodel.llmodel_isModelLoaded.argtypes = [ctypes.c_void_p] llmodel.llmodel_isModelLoaded.restype = ctypes.c_bool -PromptCallback = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_int32) -ResponseCallback = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_int32, ctypes.c_char_p) -EmbCancelCallback = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_uint), ctypes.c_uint, ctypes.c_char_p) +PromptCallback = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_int32), ctypes.c_size_t, ctypes.c_bool) +ResponseCallback = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.c_int32, ctypes.c_char_p) +EmbCancelCallback = ctypes.CFUNCTYPE(ctypes.c_bool, ctypes.POINTER(ctypes.c_uint), ctypes.c_uint, ctypes.c_char_p) +SpecialTokenCallback = ctypes.CFUNCTYPE(None, ctypes.c_char_p, ctypes.c_char_p) llmodel.llmodel_prompt.argtypes = [ ctypes.c_void_p, ctypes.c_char_p, - ctypes.c_char_p, PromptCallback, ResponseCallback, - ctypes.c_bool, ctypes.POINTER(LLModelPromptContext), - ctypes.c_bool, - ctypes.c_char_p, + ctypes.POINTER(ctypes.c_char_p), ] -llmodel.llmodel_prompt.restype = None +llmodel.llmodel_prompt.restype = ctypes.c_bool llmodel.llmodel_embed.argtypes = [ ctypes.c_void_p, @@ -222,6 +221,12 @@ class LLModelGPUDevice(ctypes.Structure): llmodel.llmodel_model_gpu_device_name.argtypes = [ctypes.c_void_p] llmodel.llmodel_model_gpu_device_name.restype = ctypes.c_char_p +llmodel.llmodel_count_prompt_tokens.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_char_p)] +llmodel.llmodel_count_prompt_tokens.restype = ctypes.c_int32 + +llmodel.llmodel_model_foreach_special_token.argtypes = [ctypes.c_void_p, SpecialTokenCallback] +llmodel.llmodel_model_foreach_special_token.restype = None + ResponseCallbackType = Callable[[int, str], bool] RawResponseCallbackType = Callable[[int, bytes], bool] EmbCancelCallbackType: TypeAlias = 'Callable[[list[int], str], bool]' @@ -266,7 +271,6 @@ def __init__(self, model_path: str, n_ctx: int, ngl: int, backend: str): self.model_path = model_path.encode() self.n_ctx = n_ctx self.ngl = ngl - self.context: LLModelPromptContext | None = None self.buffer = bytearray() self.buff_expecting_cont_bytes: int = 0 @@ -286,6 +290,10 @@ def __init__(self, model_path: str, n_ctx: int, ngl: int, backend: str): raise RuntimeError(f"Unable to instantiate model: {errmsg}") self.model: ctypes.c_void_p | None = model + self.special_tokens_map: dict[str, str] = {} + llmodel.llmodel_model_foreach_special_token( + self.model, lambda n, t: self.special_tokens_map.__setitem__(n.decode(), t.decode()), + ) def __del__(self, llmodel=llmodel): if hasattr(self, 'model'): @@ -312,6 +320,19 @@ def device(self) -> str | None: dev = llmodel.llmodel_model_gpu_device_name(self.model) return None if dev is None else dev.decode() + def count_prompt_tokens(self, prompt: str) -> int: + if self.model is None: + self._raise_closed() + err = ctypes.c_char_p() + n_tok = llmodel.llmodel_count_prompt_tokens(self.model, prompt, ctypes.byref(err)) + if n_tok < 0: + s = err.value + errmsg = 'null' if s is None else s.decode() + raise RuntimeError(f'Unable to count prompt tokens: {errmsg}') + return n_tok + + llmodel.llmodel_count_prompt_tokens.argtypes = [ctypes.c_void_p, ctypes.c_char_p] + @staticmethod def list_gpus(mem_required: int = 0) -> list[str]: """ @@ -375,48 +396,6 @@ def thread_count(self): raise Exception("Model not loaded") return llmodel.llmodel_threadCount(self.model) - def _set_context( - self, - n_predict: int = 4096, - top_k: int = 40, - top_p: float = 0.9, - min_p: float = 0.0, - temp: float = 0.1, - n_batch: int = 8, - repeat_penalty: float = 1.2, - repeat_last_n: int = 10, - context_erase: float = 0.75, - reset_context: bool = False, - ): - if self.context is None: - context = LLModelPromptContext( - n_past=0, - n_predict=n_predict, - top_k=top_k, - top_p=top_p, - min_p=min_p, - temp=temp, - n_batch=n_batch, - repeat_penalty=repeat_penalty, - repeat_last_n=repeat_last_n, - context_erase=context_erase, - ) - self.context = context - else: - context = self.context - if reset_context: - self.context.n_past = 0 - - self.context.n_predict = n_predict - self.context.top_k = top_k - self.context.top_p = top_p - self.context.min_p = min_p - self.context.temp = temp - self.context.n_batch = n_batch - self.context.repeat_penalty = repeat_penalty - self.context.repeat_last_n = repeat_last_n - self.context.context_erase = context_erase - @overload def generate_embeddings( self, text: str, prefix: str | None, dimensionality: int, do_mean: bool, atlas: bool, @@ -486,20 +465,18 @@ def wrap_cancel_cb(batch_sizes: Any, n_batch: int, backend: bytes) -> bool: def prompt_model( self, - prompt: str, - prompt_template: str, - callback: ResponseCallbackType, - n_predict: int = 4096, - top_k: int = 40, - top_p: float = 0.9, - min_p: float = 0.0, - temp: float = 0.1, - n_batch: int = 8, - repeat_penalty: float = 1.2, - repeat_last_n: int = 10, - context_erase: float = 0.75, - reset_context: bool = False, - special: bool = False, + prompt : str, + callback : ResponseCallbackType, + n_predict : int = 4096, + top_k : int = 40, + top_p : float = 0.9, + min_p : float = 0.0, + temp : float = 0.1, + n_batch : int = 8, + repeat_penalty : float = 1.2, + repeat_last_n : int = 10, + context_erase : float = 0.75, + reset_context : bool = False, ): """ Generate response from model from a prompt. @@ -522,34 +499,38 @@ def prompt_model( self.buffer.clear() self.buff_expecting_cont_bytes = 0 - self._set_context( - n_predict=n_predict, - top_k=top_k, - top_p=top_p, - min_p=min_p, - temp=temp, - n_batch=n_batch, - repeat_penalty=repeat_penalty, - repeat_last_n=repeat_last_n, - context_erase=context_erase, - reset_context=reset_context, + context = LLModelPromptContext( + n_predict = n_predict, + top_k = top_k, + top_p = top_p, + min_p = min_p, + temp = temp, + n_batch = n_batch, + repeat_penalty = repeat_penalty, + repeat_last_n = repeat_last_n, + context_erase = context_erase, ) - llmodel.llmodel_prompt( + error_msg: bytes | None = None + def error_callback(msg: bytes) -> None: + nonlocal error_msg + error_msg = msg + + err = ctypes.c_char_p() + if not llmodel.llmodel_prompt( self.model, ctypes.c_char_p(prompt.encode()), - ctypes.c_char_p(prompt_template.encode()), PromptCallback(self._prompt_callback), ResponseCallback(self._callback_decoder(callback)), - True, - self.context, - special, - ctypes.c_char_p(), - ) + context, + ctypes.byref(err), + ): + s = err.value + raise RuntimeError(f"prompt error: {'null' if s is None else s.decode()}") def prompt_model_streaming( - self, prompt: str, prompt_template: str, callback: ResponseCallbackType = empty_response_callback, **kwargs - ) -> Iterable[str]: + self, prompt: str, callback: ResponseCallbackType = empty_response_callback, **kwargs: Any, + ) -> Iterator[str]: if self.model is None: self._raise_closed() @@ -568,15 +549,15 @@ def _generator_callback(token_id: int, response: str): return _generator_callback - def run_llmodel_prompt(prompt: str, prompt_template: str, callback: ResponseCallbackType, **kwargs): - self.prompt_model(prompt, prompt_template, callback, **kwargs) + def run_llmodel_prompt(prompt: str, callback: ResponseCallbackType, **kwargs): + self.prompt_model(prompt, callback, **kwargs) output_queue.put(Sentinel.TERMINATING_SYMBOL) # Kick off llmodel_prompt in separate thread so we can return generator # immediately thread = threading.Thread( target=run_llmodel_prompt, - args=(prompt, prompt_template, _generator_callback_wrapper(callback)), + args=(prompt, _generator_callback_wrapper(callback)), kwargs=kwargs, ) thread.start() @@ -631,5 +612,5 @@ def _raw_callback(token_id: int, response: bytes) -> bool: # Empty prompt callback @staticmethod - def _prompt_callback(token_id: int) -> bool: + def _prompt_callback(token_ids: ctypes._Pointer[ctypes.c_int32], n_token_ids: int, cached: bool) -> bool: return True diff --git a/gpt4all-bindings/python/gpt4all/gpt4all.py b/gpt4all-bindings/python/gpt4all/gpt4all.py index c863817dfb07..0cd1363bca5a 100644 --- a/gpt4all-bindings/python/gpt4all/gpt4all.py +++ b/gpt4all-bindings/python/gpt4all/gpt4all.py @@ -176,6 +176,8 @@ def __init__( n_ctx: int = 2048, ngl: int = 100, verbose: bool = False, + proxies=None, + verify_ssl: bool = True ): """ Constructor @@ -201,11 +203,17 @@ def __init__( n_ctx: Maximum size of context window ngl: Number of GPU layers to use (Vulkan) verbose: If True, print debug messages. + proxies: A dictionary of proxies to be used for remote calls. + verify_ssl: If true, verify SSL certificates. Defaults to true. Note it is generally not recommended to skip SSL verification. """ + if proxies is None: + proxies = {} self.model_type = model_type self._history: list[MessageType] | None = None self._current_prompt_template: str = "{0}" + self._proxies = proxies + self._verify_ssl = verify_ssl device_init = None if sys.platform == "darwin": @@ -267,14 +275,24 @@ def current_chat_session(self) -> list[MessageType] | None: return None if self._history is None else list(self._history) @staticmethod - def list_models() -> list[ConfigType]: + def list_models( + proxies=None, + verify_ssl: bool = True + ) -> list[ConfigType]: """ Fetch model list from https://gpt4all.io/models/models3.json. + Args: + proxies: A dictionary of proxies to be used for remote calls. + verify_ssl: If true, verify SSL certificates. Defaults to true. + Returns: Model list in JSON format. """ - resp = requests.get("https://gpt4all.io/models/models3.json") + if proxies is None: + proxies = {} + + resp = requests.get("https://gpt4all.io/models/models3.json", proxies=proxies, verify=verify_ssl) if resp.status_code != 200: raise ValueError(f'Request failed: HTTP {resp.status_code} {resp.reason}') return resp.json() @@ -286,6 +304,8 @@ def retrieve_model( model_path: str | os.PathLike[str] | None = None, allow_download: bool = True, verbose: bool = False, + proxies: dict = None, + verify_ssl: bool = True, ) -> ConfigType: """ Find model file, and if it doesn't exist, download the model. @@ -296,25 +316,23 @@ def retrieve_model( ~/.cache/gpt4all/. allow_download: Allow API to download model from gpt4all.io. Default is True. verbose: If True (default), print debug messages. + proxies: A dictionary of proxies to be used for remote calls. + verify_ssl: If true, verify SSL certificates. Defaults to true. Returns: Model config. """ model_filename = append_extension_if_missing(model_name) + if proxies is None: + proxies = {} # get the config for the model config: ConfigType = {} if allow_download: - available_models = cls.list_models() - - for m in available_models: - if model_filename == m["filename"]: - tmpl = m.get("promptTemplate", DEFAULT_PROMPT_TEMPLATE) - # change to Python-style formatting - m["promptTemplate"] = tmpl.replace("%1", "{0}", 1).replace("%2", "{1}", 1) - config.update(m) - break + models = cls.list_models(proxies=proxies, verify_ssl=verify_ssl) + if (model := next((m for m in models if m["filename"] == model_filename), None)) is not None: + config.update(model) # Validate download directory if model_path is None: @@ -354,6 +372,8 @@ def download_model( url: str | None = None, expected_size: int | None = None, expected_md5: str | None = None, + proxies=None, + verify_ssl: bool = True ) -> str | os.PathLike[str]: """ Download model from gpt4all.io. @@ -365,7 +385,8 @@ def download_model( url: the models remote url (e.g. may be hosted on HF) expected_size: The expected size of the download. expected_md5: The expected MD5 hash of the download. - + proxies: A dictionary of proxies to be used for remote calls. + verify_ssl: If true, verify SSL certificates. Defaults to true. Returns: Model file destination. """ @@ -373,6 +394,8 @@ def download_model( # Download model if url is None: url = f"https://gpt4all.io/models/gguf/{model_filename}" + if proxies is None: + proxies = {} def make_request(offset=None): headers = {} @@ -380,7 +403,7 @@ def make_request(offset=None): print(f"\nDownload interrupted, resuming from byte position {offset}", file=sys.stderr) headers['Range'] = f'bytes={offset}-' # resume incomplete response headers["Accept-Encoding"] = "identity" # Content-Encoding changes meaning of ranges - response = requests.get(url, stream=True, headers=headers) + response = requests.get(url, stream=True, headers=headers, proxies=proxies, verify=verify_ssl) if response.status_code not in (200, 206): raise ValueError(f'Request failed: HTTP {response.status_code} {response.reason}') if offset and (response.status_code != 206 or str(offset) not in response.headers.get('Content-Range', '')): diff --git a/gpt4all-bindings/python/mkdocs.yml b/gpt4all-bindings/python/mkdocs.yml index a80ec9b82129..651366a32ff3 100644 --- a/gpt4all-bindings/python/mkdocs.yml +++ b/gpt4all-bindings/python/mkdocs.yml @@ -14,6 +14,7 @@ nav: - 'Models' : 'gpt4all_desktop/models.md' - 'LocalDocs' : 'gpt4all_desktop/localdocs.md' - 'Settings' : 'gpt4all_desktop/settings.md' + - 'Chat Templates' : 'gpt4all_desktop/chat_templates.md' - 'Cookbook': - 'Local AI Chat with Microsoft Excel': 'gpt4all_desktop/cookbook/use-local-ai-models-to-privately-chat-with-microsoft-excel.md' - 'Local AI Chat with your Google Drive': 'gpt4all_desktop/cookbook/use-local-ai-models-to-privately-chat-with-google-drive.md' diff --git a/gpt4all-bindings/python/setup.py b/gpt4all-bindings/python/setup.py index e96f58405fe3..b316adc0ec4b 100644 --- a/gpt4all-bindings/python/setup.py +++ b/gpt4all-bindings/python/setup.py @@ -88,9 +88,10 @@ def get_long_description(): python_requires='>=3.8', packages=find_packages(), install_requires=[ + 'importlib_resources; python_version < "3.9"', + 'jinja2~=3.1', 'requests', 'tqdm', - 'importlib_resources; python_version < "3.9"', 'typing-extensions>=4.3.0; python_version >= "3.9" and python_version < "3.11"', ], extras_require={ diff --git a/gpt4all-chat/CHANGELOG.md b/gpt4all-chat/CHANGELOG.md index ea57e14be773..579f73a3f906 100644 --- a/gpt4all-chat/CHANGELOG.md +++ b/gpt4all-chat/CHANGELOG.md @@ -6,17 +6,128 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed +- Fix "index N is not a prompt" when using LocalDocs with reasoning ([#3451](https://github.com/nomic-ai/gpt4all/pull/3451) +- Work around rendering artifacts on Snapdragon SoCs with Windows ([#3450](https://github.com/nomic-ai/gpt4all/pull/3450)) + +## [3.8.0] - 2025-01-30 + +### Added +- Support DeepSeek-R1 Qwen models ([#3431](https://github.com/nomic-ai/gpt4all/pull/3431)) +- Support for think tags in the GUI ([#3440](https://github.com/nomic-ai/gpt4all/pull/3440)) +- Support specifying SHA256 hash in models3.json instead of MD5 ([#3437](https://github.com/nomic-ai/gpt4all/pull/3437)) + +### Changed +- Use minja instead of Jinja2Cpp for significantly improved template compatibility ([#3433](https://github.com/nomic-ai/gpt4all/pull/3433)) + +### Fixed +- Fix regression while using localdocs with server API ([#3410](https://github.com/nomic-ai/gpt4all/pull/3410)) +- Don't show system messages in server chat view ([#3411](https://github.com/nomic-ai/gpt4all/pull/3411)) +- Fix `codesign --verify` failure on macOS ([#3413](https://github.com/nomic-ai/gpt4all/pull/3413)) +- Code Interpreter: Fix console.log not accepting a single string after v3.7.0 ([#3426](https://github.com/nomic-ai/gpt4all/pull/3426)) +- Fix Phi 3.1 Mini 128K Instruct template (by [@ThiloteE](https://github.com/ThiloteE) in [#3412](https://github.com/nomic-ai/gpt4all/pull/3412)) +- Don't block the gui thread for reasoning ([#3435](https://github.com/nomic-ai/gpt4all/pull/3435)) +- Fix corruption of unicode in output of reasoning models ([#3443](https://github.com/nomic-ai/gpt4all/pull/3443)) + +## [3.7.0] - 2025-01-21 + +### Added +- Add support for the Windows ARM64 target platform (CPU-only) ([#3385](https://github.com/nomic-ai/gpt4all/pull/3385)) + +### Changed +- Update from Qt 6.5.1 to 6.8.1 ([#3386](https://github.com/nomic-ai/gpt4all/pull/3386)) + +### Fixed +- Fix the timeout error in code interpreter ([#3369](https://github.com/nomic-ai/gpt4all/pull/3369)) +- Fix code interpreter console.log not accepting multiple arguments ([#3371](https://github.com/nomic-ai/gpt4all/pull/3371)) +- Remove 'X is defined' checks from templates for better compatibility ([#3372](https://github.com/nomic-ai/gpt4all/pull/3372)) +- Jinja2Cpp: Add 'if' requirement for 'else' parsing to fix crash ([#3373](https://github.com/nomic-ai/gpt4all/pull/3373)) +- Save chats on quit, even if the window isn't closed first ([#3387](https://github.com/nomic-ai/gpt4all/pull/3387)) +- Add chat template replacements for five new models and fix EM German Mistral ([#3393](https://github.com/nomic-ai/gpt4all/pull/3393)) +- Fix crash when entering `{{ a["foo"(` as chat template ([#3394](https://github.com/nomic-ai/gpt4all/pull/3394)) +- Sign the maintenance tool on macOS to prevent crash on Sequoia ([#3391](https://github.com/nomic-ai/gpt4all/pull/3391)) +- Jinja2Cpp: Fix operator precedence in 'not X is defined' ([#3402](https://github.com/nomic-ai/gpt4all/pull/3402)) + +## [3.6.1] - 2024-12-20 + +### Fixed +- Fix the stop generation button no longer working in v3.6.0 ([#3336](https://github.com/nomic-ai/gpt4all/pull/3336)) +- Fix the copy entire conversation button no longer working in v3.6.0 ([#3336](https://github.com/nomic-ai/gpt4all/pull/3336)) + +## [3.6.0] - 2024-12-19 + +### Added +- Automatically substitute chat templates that are not compatible with Jinja2Cpp in GGUFs ([#3327](https://github.com/nomic-ai/gpt4all/pull/3327)) +- Built-in javascript code interpreter tool plus model ([#3173](https://github.com/nomic-ai/gpt4all/pull/3173)) + +### Fixed +- Fix remote model template to allow for XML in messages ([#3318](https://github.com/nomic-ai/gpt4all/pull/3318)) +- Fix Jinja2Cpp bug that broke system message detection in chat templates ([#3325](https://github.com/nomic-ai/gpt4all/pull/3325)) +- Fix LocalDocs sources displaying in unconsolidated form after v3.5.0 ([#3328](https://github.com/nomic-ai/gpt4all/pull/3328)) + +## [3.5.3] - 2024-12-16 + +### Fixed +- Fix LocalDocs not using information from sources in v3.5.2 ([#3302](https://github.com/nomic-ai/gpt4all/pull/3302)) + +## [3.5.2] - 2024-12-13 + +### Added +- Create separate download pages for built-in and HuggingFace models ([#3269](https://github.com/nomic-ai/gpt4all/pull/3269)) + +### Fixed +- Fix API server ignoring assistant messages in history after v3.5.0 ([#3256](https://github.com/nomic-ai/gpt4all/pull/3256)) +- Fix API server replying with incorrect token counts and stop reason after v3.5.0 ([#3256](https://github.com/nomic-ai/gpt4all/pull/3256)) +- Fix API server remembering previous, unrelated conversations after v3.5.0 ([#3256](https://github.com/nomic-ai/gpt4all/pull/3256)) +- Fix mishandling of default chat template and system message of cloned models in v3.5.0 ([#3262](https://github.com/nomic-ai/gpt4all/pull/3262)) +- Fix untranslated text on the startup dialog ([#3293](https://github.com/nomic-ai/gpt4all/pull/3293)) + +## [3.5.1] - 2024-12-10 + +### Fixed +- Fix an incorrect value for currentResponse ([#3245](https://github.com/nomic-ai/gpt4all/pull/3245)) +- Fix the default model button so it works again after 3.5.0 ([#3246](https://github.com/nomic-ai/gpt4all/pull/3246)) +- Fix chat templates for Nous Hermes 2 Mistral, Mistral OpenOrca, Qwen 2, and remote models ([#3250](https://github.com/nomic-ai/gpt4all/pull/3250)) +- Fix chat templates for Llama 3.2 models ([#3251](https://github.com/nomic-ai/gpt4all/pull/3251)) + +## [3.5.0] - 2024-12-09 + +### Changed +- Update Italian translation (by [@Harvester62](https://github.com/Harvester62) in [#3236](https://github.com/nomic-ai/gpt4all/pull/3236)) +- Update Romanian translation (by [@SINAPSA-IC](https://github.com/SINAPSA-IC) in [#3232](https://github.com/nomic-ai/gpt4all/pull/3232)) + +### Fixed +- Fix a few more problems with the Jinja changes ([#3239](https://github.com/nomic-ai/gpt4all/pull/3239)) + +## [3.5.0-rc2] - 2024-12-06 + +### Changed +- Fade messages out with an animation when they are removed from the chat view ([#3227](https://github.com/nomic-ai/gpt4all/pull/3227)) +- Tweak wording of edit/redo confirmation dialogs ([#3228](https://github.com/nomic-ai/gpt4all/pull/3228)) +- Make edit/redo buttons disabled instead of invisible when they are temporarily unavailable ([#3228](https://github.com/nomic-ai/gpt4all/pull/3228)) + +## [3.5.0-rc1] - 2024-12-04 + ### Added - Add ability to attach text, markdown, and rst files to chat ([#3135](https://github.com/nomic-ai/gpt4all/pull/3135)) -- Add feature to minimize to system tray (by [@bgallois](https://github.com/bgallois) in ([#3109](https://github.com/nomic-ai/gpt4all/pull/3109)) +- Add feature to minimize to system tray (by [@bgallois](https://github.com/bgallois) in [#3109](https://github.com/nomic-ai/gpt4all/pull/3109)) - Basic cache for faster prefill when the input shares a prefix with previous context ([#3073](https://github.com/nomic-ai/gpt4all/pull/3073)) +- Add ability to edit prompts and regenerate any response ([#3147](https://github.com/nomic-ai/gpt4all/pull/3147)) ### Changed - Implement Qt 6.8 compatibility ([#3121](https://github.com/nomic-ai/gpt4all/pull/3121)) +- Use Jinja for chat templates instead of per-message QString.arg-style templates ([#3147](https://github.com/nomic-ai/gpt4all/pull/3147)) +- API server: Use system message(s) from client instead of settings ([#3147](https://github.com/nomic-ai/gpt4all/pull/3147)) +- API server: Accept messages in any order supported by the model instead of requiring user/assistant pairs ([#3147](https://github.com/nomic-ai/gpt4all/pull/3147)) +- Remote models: Pass system message with "system" role instead of joining with user message ([#3147](https://github.com/nomic-ai/gpt4all/pull/3147)) + +### Removed +- Remove option to save binary model state to disk ([#3147](https://github.com/nomic-ai/gpt4all/pull/3147)) ### Fixed - Fix bug in GUI when localdocs encounters binary data ([#3137](https://github.com/nomic-ai/gpt4all/pull/3137)) - Fix LocalDocs bugs that prevented some docx files from fully chunking ([#3140](https://github.com/nomic-ai/gpt4all/pull/3140)) +- Fix missing softmax that was causing crashes and effectively infinite temperature since 3.4.0 ([#3202](https://github.com/nomic-ai/gpt4all/pull/3202)) ## [3.4.2] - 2024-10-16 @@ -178,7 +289,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Fix several Vulkan resource management issues ([#2694](https://github.com/nomic-ai/gpt4all/pull/2694)) - Fix crash/hang when some models stop generating, by showing special tokens ([#2701](https://github.com/nomic-ai/gpt4all/pull/2701)) -[Unreleased]: https://github.com/nomic-ai/gpt4all/compare/v3.4.2...HEAD +[Unreleased]: https://github.com/nomic-ai/gpt4all/compare/v3.8.0...HEAD +[3.8.0]: https://github.com/nomic-ai/gpt4all/compare/v3.7.0...v3.8.0 +[3.7.0]: https://github.com/nomic-ai/gpt4all/compare/v3.6.1...v3.7.0 +[3.6.1]: https://github.com/nomic-ai/gpt4all/compare/v3.6.0...v3.6.1 +[3.6.0]: https://github.com/nomic-ai/gpt4all/compare/v3.5.3...v3.6.0 +[3.5.3]: https://github.com/nomic-ai/gpt4all/compare/v3.5.2...v3.5.3 +[3.5.2]: https://github.com/nomic-ai/gpt4all/compare/v3.5.1...v3.5.2 +[3.5.1]: https://github.com/nomic-ai/gpt4all/compare/v3.5.0...v3.5.1 +[3.5.0]: https://github.com/nomic-ai/gpt4all/compare/v3.5.0-rc2...v3.5.0 +[3.5.0-rc2]: https://github.com/nomic-ai/gpt4all/compare/v3.5.0-rc1...v3.5.0-rc2 +[3.5.0-rc1]: https://github.com/nomic-ai/gpt4all/compare/v3.4.2...v3.5.0-rc1 [3.4.2]: https://github.com/nomic-ai/gpt4all/compare/v3.4.1...v3.4.2 [3.4.1]: https://github.com/nomic-ai/gpt4all/compare/v3.4.0...v3.4.1 [3.4.0]: https://github.com/nomic-ai/gpt4all/compare/v3.3.0...v3.4.0 diff --git a/gpt4all-chat/CMakeLists.txt b/gpt4all-chat/CMakeLists.txt index 9ae28ce9aca6..6f6004d8a913 100644 --- a/gpt4all-chat/CMakeLists.txt +++ b/gpt4all-chat/CMakeLists.txt @@ -3,13 +3,17 @@ cmake_minimum_required(VERSION 3.25) # for try_compile SOURCE_FROM_VAR include(../common/common.cmake) set(APP_VERSION_MAJOR 3) -set(APP_VERSION_MINOR 4) -set(APP_VERSION_PATCH 3) +set(APP_VERSION_MINOR 8) +set(APP_VERSION_PATCH 1) set(APP_VERSION_BASE "${APP_VERSION_MAJOR}.${APP_VERSION_MINOR}.${APP_VERSION_PATCH}") set(APP_VERSION "${APP_VERSION_BASE}-dev0") project(gpt4all VERSION ${APP_VERSION_BASE} LANGUAGES CXX C) +if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install CACHE PATH "..." FORCE) +endif() + if(APPLE) option(BUILD_UNIVERSAL "Build a Universal binary on macOS" OFF) if(BUILD_UNIVERSAL) @@ -28,11 +32,30 @@ option(GPT4ALL_TEST "Build the tests" ${Python3_FOUND}) option(GPT4ALL_LOCALHOST "Build installer for localhost repo" OFF) option(GPT4ALL_OFFLINE_INSTALLER "Build an offline installer" OFF) option(GPT4ALL_SIGN_INSTALL "Sign installed binaries and installers (requires signing identities)" OFF) - +option(GPT4ALL_GEN_CPACK_CONFIG "Generate the CPack config.xml in the package step and nothing else." OFF) +set(GPT4ALL_USE_QTPDF "AUTO" CACHE STRING "Whether to Use QtPDF for LocalDocs. If OFF or not available on this platform, PDFium is used.") +set_property(CACHE GPT4ALL_USE_QTPDF PROPERTY STRINGS AUTO ON OFF) +set(GPT4ALL_FORCE_D3D12 "AUTO" CACHE STRING "Whether to use Direct3D 12 as the Qt scene graph backend. Defaults to ON on Windows ARM.") +set_property(CACHE GPT4ALL_FORCE_D3D12 PROPERTY STRINGS AUTO ON OFF) + +include(cmake/cpack_config.cmake) + +if (GPT4ALL_GEN_CPACK_CONFIG) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/cpack-steal-config.cmake.in" + "${CMAKE_BINARY_DIR}/cmake/cpack-steal-config.cmake" @ONLY) + set(CPACK_POST_BUILD_SCRIPTS ${CMAKE_BINARY_DIR}/cmake/cpack-steal-config.cmake) + include(CPack) + include(CPackIFW) + return() +endif() set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) +if (MSVC) + # Enable accurate __cplusplus macro + add_compile_options($<$:/Zc:__cplusplus>) +endif() # conftests @@ -69,14 +92,19 @@ include_directories("${CMAKE_CURRENT_BINARY_DIR}") set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -# Generate a header file with the version number -configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/cmake/config.h.in" - "${CMAKE_CURRENT_BINARY_DIR}/config.h" -) - set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON) -find_package(Qt6 6.5 COMPONENTS Core HttpServer LinguistTools Pdf Quick QuickDialogs2 Sql Svg REQUIRED) +set(GPT4ALL_QT_COMPONENTS Core HttpServer LinguistTools Quick QuickDialogs2 Sql Svg) +set(GPT4ALL_USING_QTPDF OFF) +if (CMAKE_SYSTEM_NAME MATCHES Windows AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|AARCH64|arm64|ARM64)$") + # QtPDF is not available. + if (GPT4ALL_USE_QTPDF STREQUAL "ON") + message(FATAL_ERROR "QtPDF is not available on Windows ARM64.") + endif() +elseif (GPT4ALL_USE_QTPDF MATCHES "^(ON|AUTO)$") + set(GPT4ALL_USING_QTPDF ON) + list(APPEND GPT4ALL_QT_COMPONENTS Pdf) +endif() +find_package(Qt6 6.5 COMPONENTS ${GPT4ALL_QT_COMPONENTS} REQUIRED) if (QT_KNOWN_POLICY_QTP0004) qt_policy(SET QTP0004 NEW) # generate extra qmldir files on Qt 6.8+ @@ -98,6 +126,24 @@ message(STATUS "Qt 6 root directory: ${Qt6_ROOT_DIR}") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(GPT4ALL_CONFIG_FORCE_D3D12 -1) +if (NOT CMAKE_SYSTEM_NAME MATCHES Windows OR Qt6_VERSION VERSION_LESS "6.6") + # Direct3D 12 is not available. + if (GPT4ALL_FORCE_D3D12 STREQUAL "ON") + message(FATAL_ERROR "Cannot use Direct3D 12 on this platform.") + endif() +elseif (GPT4ALL_FORCE_D3D12 MATCHES "^(ON|AUTO)$") + if (GPT4ALL_FORCE_D3D12 STREQUAL "ON" OR CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|AARCH64|arm64|ARM64)$") + set(GPT4ALL_CONFIG_FORCE_D3D12 1) + endif() +endif() + +# Generate a header file for configuration +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/src/config.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/config.h" +) + add_subdirectory(deps) add_subdirectory(../gpt4all-backend llmodel) @@ -185,11 +231,14 @@ qt_add_executable(chat src/chatapi.cpp src/chatapi.h src/chatlistmodel.cpp src/chatlistmodel.h src/chatllm.cpp src/chatllm.h - src/chatmodel.h + src/chatmodel.h src/chatmodel.cpp src/chatviewtextprocessor.cpp src/chatviewtextprocessor.h + src/codeinterpreter.cpp src/codeinterpreter.h src/database.cpp src/database.h src/download.cpp src/download.h src/embllm.cpp src/embllm.h + src/jinja_helpers.cpp src/jinja_helpers.h + src/jinja_replacements.cpp src/jinja_replacements.h src/llm.cpp src/llm.h src/localdocs.cpp src/localdocs.h src/localdocsmodel.cpp src/localdocsmodel.h @@ -198,6 +247,9 @@ qt_add_executable(chat src/mysettings.cpp src/mysettings.h src/network.cpp src/network.h src/server.cpp src/server.h + src/tool.cpp src/tool.h + src/toolcallparser.cpp src/toolcallparser.h + src/toolmodel.cpp src/toolmodel.h src/xlsxtomd.cpp src/xlsxtomd.h ${CHAT_EXE_RESOURCES} ${MACOS_SOURCES} @@ -212,9 +264,14 @@ qt_add_qml_module(chat main.qml qml/AddCollectionView.qml qml/AddModelView.qml + qml/AddGPT4AllModelView.qml + qml/AddHFModelView.qml qml/ApplicationSettings.qml qml/ChatDrawer.qml + qml/ChatCollapsibleItem.qml qml/ChatItemView.qml + qml/ChatMessageButton.qml + qml/ChatTextItem.qml qml/ChatView.qml qml/CollectionsDrawer.qml qml/HomeView.qml @@ -227,13 +284,14 @@ qt_add_qml_module(chat qml/PopupDialog.qml qml/SettingsView.qml qml/StartupDialog.qml - qml/SwitchModelDialog.qml + qml/ConfirmationDialog.qml qml/Theme.qml qml/ThumbsDownDialog.qml qml/Toast.qml qml/ToastManager.qml qml/MyBusyIndicator.qml qml/MyButton.qml + qml/MyTabButton.qml qml/MyCheckBox.qml qml/MyComboBox.qml qml/MyDialog.qml @@ -384,9 +442,19 @@ target_include_directories(chat PRIVATE deps/usearch/include deps/usearch/fp16/include) target_link_libraries(chat - PRIVATE Qt6::Core Qt6::HttpServer Qt6::Pdf Qt6::Quick Qt6::Sql Qt6::Svg) + PRIVATE Qt6::Core Qt6::HttpServer Qt6::Quick Qt6::Sql Qt6::Svg) +if (GPT4ALL_USING_QTPDF) + target_compile_definitions(chat PRIVATE GPT4ALL_USE_QTPDF) + target_link_libraries(chat PRIVATE Qt6::Pdf) +else() + # Link PDFium + target_link_libraries(chat PRIVATE pdfium) +endif() target_link_libraries(chat PRIVATE llmodel SingleApplication fmt::fmt duckx::duckx QXlsx) +target_include_directories(chat PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/json/include) +target_include_directories(chat PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/json/include/nlohmann) +target_include_directories(chat PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/minja/include) if (APPLE) target_link_libraries(chat PRIVATE ${COCOA_LIBRARY}) @@ -394,18 +462,18 @@ endif() # -- install -- -set(COMPONENT_NAME_MAIN ${PROJECT_NAME}) - -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install CACHE PATH "..." FORCE) +if (APPLE) + set(GPT4ALL_LIB_DEST bin/gpt4all.app/Contents/Frameworks) +else() + set(GPT4ALL_LIB_DEST lib) endif() install(TARGETS chat DESTINATION bin COMPONENT ${COMPONENT_NAME_MAIN}) install( TARGETS llmodel - LIBRARY DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN} # .so/.dylib - RUNTIME DESTINATION bin COMPONENT ${COMPONENT_NAME_MAIN} # .dll + LIBRARY DESTINATION ${GPT4ALL_LIB_DEST} COMPONENT ${COMPONENT_NAME_MAIN} # .so/.dylib + RUNTIME DESTINATION bin COMPONENT ${COMPONENT_NAME_MAIN} # .dll ) # We should probably iterate through the list of the cmake for backend, but these need to be installed @@ -428,8 +496,8 @@ endif() install( TARGETS ${MODEL_IMPL_TARGETS} - LIBRARY DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN} # .so/.dylib - RUNTIME DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN} # .dll + LIBRARY DESTINATION ${GPT4ALL_LIB_DEST} COMPONENT ${COMPONENT_NAME_MAIN} # .so/.dylib + RUNTIME DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN} # .dll ) if(APPLE AND GPT4ALL_SIGN_INSTALL) @@ -458,7 +526,7 @@ if (LLMODEL_CUDA) TARGETS llamamodel-mainline-cuda llamamodel-mainline-cuda-avxonly RUNTIME_DEPENDENCY_SET llama-cuda-deps - LIBRARY DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN} # .so/.dylib + LIBRARY DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN} # .so RUNTIME DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN} # .dll ) if (WIN32) @@ -472,66 +540,38 @@ if (LLMODEL_CUDA) endif() endif() +if (NOT GPT4ALL_USING_QTPDF) + # Install PDFium + if (WIN32) + install(FILES ${PDFium_LIBRARY} DESTINATION bin COMPONENT ${COMPONENT_NAME_MAIN}) # .dll + else() + install(FILES ${PDFium_LIBRARY} DESTINATION ${GPT4ALL_LIB_DEST} COMPONENT ${COMPONENT_NAME_MAIN}) # .so/.dylib + endif() +endif() + if (NOT APPLE) install(FILES "${LOCAL_EMBEDDING_MODEL_PATH}" DESTINATION resources COMPONENT ${COMPONENT_NAME_MAIN}) endif() -set(CPACK_GENERATOR "IFW") -set(CPACK_VERBATIM_VARIABLES YES) -set(CPACK_IFW_VERBOSE ON) - -if(${CMAKE_SYSTEM_NAME} MATCHES Linux) +if (CMAKE_SYSTEM_NAME MATCHES Linux) find_program(LINUXDEPLOYQT linuxdeployqt HINTS "$ENV{HOME}/dev/linuxdeployqt/build/tools/linuxdeployqt" "$ENV{HOME}/project/linuxdeployqt/bin") configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/deploy-qt-linux.cmake.in" "${CMAKE_BINARY_DIR}/cmake/deploy-qt-linux.cmake" @ONLY) set(CPACK_PRE_BUILD_SCRIPTS ${CMAKE_BINARY_DIR}/cmake/deploy-qt-linux.cmake) - set(CPACK_IFW_ROOT "~/Qt/Tools/QtInstallerFramework/4.6") - set(CPACK_PACKAGE_FILE_NAME "${COMPONENT_NAME_MAIN}-installer-linux") - set(CPACK_IFW_TARGET_DIRECTORY "@HomeDir@/${COMPONENT_NAME_MAIN}") -elseif(${CMAKE_SYSTEM_NAME} MATCHES Windows) - find_program(WINDEPLOYQT windeployqt HINTS ${_qt_bin_dir}) +elseif (CMAKE_SYSTEM_NAME MATCHES Windows) + find_program(WINDEPLOYQT windeployqt) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/deploy-qt-windows.cmake.in" "${CMAKE_BINARY_DIR}/cmake/deploy-qt-windows.cmake" @ONLY) set(CPACK_PRE_BUILD_SCRIPTS ${CMAKE_BINARY_DIR}/cmake/deploy-qt-windows.cmake) - set(CPACK_IFW_ROOT "C:/Qt/Tools/QtInstallerFramework/4.6") - set(CPACK_IFW_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.ico") - set(CPACK_PACKAGE_FILE_NAME "${COMPONENT_NAME_MAIN}-installer-win64") - set(CPACK_IFW_TARGET_DIRECTORY "@HomeDir@\\${COMPONENT_NAME_MAIN}") -elseif(${CMAKE_SYSTEM_NAME} MATCHES Darwin) - find_program(MACDEPLOYQT macdeployqt HINTS ${_qt_bin_dir}) +elseif (CMAKE_SYSTEM_NAME MATCHES Darwin) + find_program(MACDEPLOYQT macdeployqt) configure_file("${CMAKE_CURRENT_SOURCE_DIR}/cmake/deploy-qt-mac.cmake.in" "${CMAKE_BINARY_DIR}/cmake/deploy-qt-mac.cmake" @ONLY) set(CPACK_PRE_BUILD_SCRIPTS ${CMAKE_BINARY_DIR}/cmake/deploy-qt-mac.cmake) - set(CPACK_IFW_ROOT "~/Qt/Tools/QtInstallerFramework/4.6") - set(CPACK_IFW_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.icns") - set(CPACK_PACKAGE_FILE_NAME "${COMPONENT_NAME_MAIN}-installer-darwin") - set(CPACK_IFW_TARGET_DIRECTORY "@ApplicationsDir@/${COMPONENT_NAME_MAIN}") - set(CPACK_BUNDLE_NAME ${COMPONENT_NAME_MAIN}) - set(CPACK_BUNDLE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.icns") endif() -set(CPACK_PACKAGE_INSTALL_DIRECTORY ${COMPONENT_NAME_MAIN}) -set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) -set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) -SET(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) -set(CPACK_PACKAGE_HOMEPAGE_URL "https://www.nomic.ai/gpt4all") -set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-48.png") -set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE) -set(CPACK_RESOURCE_FILE_README ${CMAKE_CURRENT_SOURCE_DIR}/README.md) -set(CPACK_PACKAGE_EXECUTABLES "GPT4All") -set(CPACK_CREATE_DESKTOP_LINKS "GPT4All") -set(CPACK_IFW_PACKAGE_NAME "GPT4All") -set(CPACK_IFW_PACKAGE_TITLE "GPT4All Installer") -set(CPACK_IFW_PACKAGE_PUBLISHER "Nomic, Inc.") -set(CPACK_IFW_PRODUCT_URL "https://www.nomic.ai/gpt4all") -set(CPACK_IFW_PACKAGE_WIZARD_STYLE "Aero") -set(CPACK_IFW_PACKAGE_LOGO "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-48.png") -set(CPACK_IFW_PACKAGE_WINDOW_ICON "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-32.png") -set(CPACK_IFW_PACKAGE_WIZARD_SHOW_PAGE_LIST OFF) -set(CPACK_IFW_PACKAGE_CONTROL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/installer_control.qs") - include(InstallRequiredSystemLibraries) include(CPack) include(CPackIFW) @@ -543,20 +583,35 @@ endif() cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} ESSENTIAL FORCED_INSTALLATION) cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} VERSION ${APP_VERSION}) cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} LICENSES "MIT LICENSE" ${CPACK_RESOURCE_FILE_LICENSE}) -cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/installer_component.qs") +cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/installer_gpt4all_component.qs") cpack_ifw_configure_component(${COMPONENT_NAME_MAIN} REPLACES "gpt4all-chat") #Was used in very earliest prototypes +if (APPLE AND GPT4ALL_SIGN_INSTALL) + if (GPT4ALL_OFFLINE_INSTALLER) + cpack_add_component(maintenancetool HIDDEN) + else() + cpack_add_component(maintenancetool HIDDEN DOWNLOADED) + endif() + cpack_ifw_configure_component(maintenancetool ESSENTIAL FORCED_INSTALLATION) + cpack_ifw_configure_component(maintenancetool VERSION ${APP_VERSION}) + cpack_ifw_configure_component(maintenancetool SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/installer_maintenancetool_component.qs") +endif() + if (GPT4ALL_LOCALHOST) cpack_ifw_add_repository("GPT4AllRepository" URL "http://localhost/repository") -elseif(GPT4ALL_OFFLINE_INSTALLER) - add_compile_definitions(GPT4ALL_OFFLINE_INSTALLER) +elseif (GPT4ALL_OFFLINE_INSTALLER) + add_compile_definitions(GPT4ALL_OFFLINE_INSTALLER) else() - if(${CMAKE_SYSTEM_NAME} MATCHES Linux) - cpack_ifw_add_repository("GPT4AllRepository" URL "https://gpt4all.io/installer_repos/linux/repository") - elseif(${CMAKE_SYSTEM_NAME} MATCHES Windows) - #To sign the target on windows have to create a batch script add use it as a custom target and then use CPACK_IFW_EXTRA_TARGETS to set this extra target - cpack_ifw_add_repository("GPT4AllRepository" URL "https://gpt4all.io/installer_repos/windows/repository") - elseif(${CMAKE_SYSTEM_NAME} MATCHES Darwin) - cpack_ifw_add_repository("GPT4AllRepository" URL "https://gpt4all.io/installer_repos/mac/repository") - endif() + if (CMAKE_SYSTEM_NAME MATCHES Linux) + cpack_ifw_add_repository("GPT4AllRepository" URL "https://gpt4all.io/installer_repos/linux/repository") + elseif (CMAKE_SYSTEM_NAME MATCHES Windows) + # To sign the target on windows have to create a batch script add use it as a custom target and then use CPACK_IFW_EXTRA_TARGETS to set this extra target + if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$") + cpack_ifw_add_repository("GPT4AllRepository" URL "https://gpt4all.io/installer_repos/windows/repository") + elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|AARCH64|arm64|ARM64)$") + cpack_ifw_add_repository("GPT4AllRepository" URL "https://gpt4all.io/installer_repos/windows_arm/repository") + endif() + elseif (CMAKE_SYSTEM_NAME MATCHES Darwin) + cpack_ifw_add_repository("GPT4AllRepository" URL "https://gpt4all.io/installer_repos/mac/repository") + endif() endif() diff --git a/gpt4all-chat/README.md b/gpt4all-chat/README.md deleted file mode 100644 index eca85e525821..000000000000 --- a/gpt4all-chat/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# gpt4all-chat - -Cross platform Qt based GUI for GPT4All versions with GPT-J as the base -model. NOTE: The model seen in the screenshot is actually a preview of a -new training run for GPT4All based on GPT-J. The GPT4All project is busy -at work getting ready to release this model including installers for all -three major OS's. In the meantime, you can try this UI out with the original -GPT-J model by following build instructions below. - -![image](https://user-images.githubusercontent.com/50458173/231464085-da9edff6-a593-410e-8f38-7513f75c8aab.png) - -## Install - -One click installers for macOS, Linux, and Windows at https://www.nomic.ai/gpt4all - -## Features - -* Cross-platform (Linux, Windows, MacOSX) -* The UI is made to look and feel like you've come to expect from a chatty gpt -* Check for updates so you can always stay fresh with latest models -* Easy to install with precompiled binaries available for all three major desktop platforms -* Multi-modal - Ability to load more than one model and switch between them -* Multi-chat - a list of current and past chats and the ability to save/delete/export and switch between -* Supports models that are supported by llama.cpp -* Model downloader in GUI featuring many popular open source models -* Settings dialog to change temp, top_p, min_p, top_k, threads, etc -* Copy your conversation to clipboard -* RAG via LocalDocs feature -* Check for updates to get the very latest GUI - -## Building and running - -* Follow the visual instructions on the [build_and_run](build_and_run.md) page - -## Getting the latest - -If you've already checked out the source code and/or built the program make sure when you do a git fetch to get the latest changes and that you also do `git submodule update --init --recursive` to update the submodules. (If you ever run into trouble, deinitializing via `git submodule deinit -f .` and then initializing again via `git submodule update --init --recursive` fixes most issues) - -## Contributing - -* Pull requests welcome. See the feature wish list for ideas :) - - -## License -The source code of this chat interface is currently under a MIT license. diff --git a/gpt4all-chat/cmake/config.h.in b/gpt4all-chat/cmake/config.h.in deleted file mode 100644 index c6b77b5bc449..000000000000 --- a/gpt4all-chat/cmake/config.h.in +++ /dev/null @@ -1,6 +0,0 @@ -#ifndef CONFIG_H -#define CONFIG_H - -#define APP_VERSION "@APP_VERSION@" - -#endif // CONFIG_H diff --git a/gpt4all-chat/cmake/cpack-steal-config.cmake.in b/gpt4all-chat/cmake/cpack-steal-config.cmake.in new file mode 100644 index 000000000000..41d15ee8e271 --- /dev/null +++ b/gpt4all-chat/cmake/cpack-steal-config.cmake.in @@ -0,0 +1,2 @@ +set(OUTPUT_DIR "@CMAKE_BINARY_DIR@") +file(COPY ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/config DESTINATION ${OUTPUT_DIR}/cpack-config) diff --git a/gpt4all-chat/cmake/cpack_config.cmake b/gpt4all-chat/cmake/cpack_config.cmake new file mode 100644 index 000000000000..c68b73501183 --- /dev/null +++ b/gpt4all-chat/cmake/cpack_config.cmake @@ -0,0 +1,50 @@ +set(COMPONENT_NAME_MAIN "gpt4all") + +set(CPACK_GENERATOR "IFW") +set(CPACK_VERBATIM_VARIABLES YES) +set(CPACK_IFW_VERBOSE ON) + +if (CMAKE_SYSTEM_NAME MATCHES Linux) + set(CPACK_IFW_ROOT "~/Qt/Tools/QtInstallerFramework/4.6") + set(CPACK_PACKAGE_FILE_NAME "${COMPONENT_NAME_MAIN}-installer-linux") + set(CPACK_IFW_TARGET_DIRECTORY "@HomeDir@/${COMPONENT_NAME_MAIN}") +elseif (CMAKE_SYSTEM_NAME MATCHES Windows) + set(CPACK_IFW_ROOT "C:/Qt/Tools/QtInstallerFramework/4.6") + set(CPACK_IFW_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.ico") + if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$") + set(CPACK_PACKAGE_FILE_NAME "${COMPONENT_NAME_MAIN}-installer-win64") + elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|AARCH64|arm64|ARM64)$") + set(CPACK_PACKAGE_FILE_NAME "${COMPONENT_NAME_MAIN}-installer-win64-arm") + else() + message(FATAL_ERROR "Unrecognized processor: ${CMAKE_SYSTEM_PROCESSOR}") + endif() + set(CPACK_IFW_TARGET_DIRECTORY "@HomeDir@\\${COMPONENT_NAME_MAIN}") +elseif (CMAKE_SYSTEM_NAME MATCHES Darwin) + set(CPACK_IFW_ROOT "~/Qt/Tools/QtInstallerFramework/4.6") + set(CPACK_IFW_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.icns") + set(CPACK_PACKAGE_FILE_NAME "${COMPONENT_NAME_MAIN}-installer-darwin") + set(CPACK_IFW_TARGET_DIRECTORY "@ApplicationsDir@/${COMPONENT_NAME_MAIN}") +endif() + +set(CPACK_COMPONENTS_ALL ${COMPONENT_NAME_MAIN}) # exclude development components +if (APPLE AND GPT4ALL_SIGN_INSTALL) + list(APPEND CPACK_COMPONENTS_ALL maintenancetool) +endif() +set(CPACK_PACKAGE_INSTALL_DIRECTORY ${COMPONENT_NAME_MAIN}) +set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) +set(CPACK_PACKAGE_HOMEPAGE_URL "https://www.nomic.ai/gpt4all") +set(CPACK_PACKAGE_ICON "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-48.png") +set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE) +set(CPACK_PACKAGE_EXECUTABLES "GPT4All") +set(CPACK_CREATE_DESKTOP_LINKS "GPT4All") +set(CPACK_IFW_PACKAGE_NAME "GPT4All") +set(CPACK_IFW_PACKAGE_TITLE "GPT4All Installer") +set(CPACK_IFW_PACKAGE_PUBLISHER "Nomic, Inc.") +set(CPACK_IFW_PRODUCT_URL "https://www.nomic.ai/gpt4all") +set(CPACK_IFW_PACKAGE_WIZARD_STYLE "Aero") +set(CPACK_IFW_PACKAGE_LOGO "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-48.png") +set(CPACK_IFW_PACKAGE_WINDOW_ICON "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-32.png") +set(CPACK_IFW_PACKAGE_WIZARD_SHOW_PAGE_LIST OFF) +set(CPACK_IFW_PACKAGE_CONTROL_SCRIPT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/installer_control.qs") diff --git a/gpt4all-chat/cmake/deploy-qt-mac.cmake.in b/gpt4all-chat/cmake/deploy-qt-mac.cmake.in index d9458aa32c40..8798e21e7083 100644 --- a/gpt4all-chat/cmake/deploy-qt-mac.cmake.in +++ b/gpt4all-chat/cmake/deploy-qt-mac.cmake.in @@ -1,20 +1,26 @@ set(MACDEPLOYQT "@MACDEPLOYQT@") set(COMPONENT_NAME_MAIN "@COMPONENT_NAME_MAIN@") set(CMAKE_CURRENT_SOURCE_DIR "@CMAKE_CURRENT_SOURCE_DIR@") +set(GPT4ALL_SIGN_INSTALL "@GPT4ALL_SIGN_INSTALL@") set(GPT4ALL_SIGNING_ID "@MAC_SIGNING_IDENTITY@") -if (GPT4ALL_SIGNING_ID) +set(CPACK_CONFIG_DIR "@CMAKE_BINARY_DIR@") +if (GPT4ALL_SIGN_INSTALL) set(MAC_NOTARIZE -sign-for-notarization=${GPT4ALL_SIGNING_ID}) endif() execute_process(COMMAND ${MACDEPLOYQT} ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/bin/gpt4all.app -qmldir=${CMAKE_CURRENT_SOURCE_DIR} -verbose=2 ${MAC_NOTARIZE}) -file(GLOB MYLLAMALIBS ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/lib/libllama*) -file(GLOB MYLLMODELLIBS ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/lib/libllmodel.*) -file(COPY ${MYLLAMALIBS} - DESTINATION ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/bin/gpt4all.app/Contents/Frameworks) -file(COPY ${MYLLMODELLIBS} - DESTINATION ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data/bin/gpt4all.app/Contents/Frameworks) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-32.png" DESTINATION ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/icons/gpt4all-48.png" DESTINATION ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources/gpt4all.icns" DESTINATION ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/${COMPONENT_NAME_MAIN}/data) + +if (GPT4ALL_SIGN_INSTALL) + # Create signed MaintenanceTool + set(MT_DATA_DIR ${CPACK_TEMPORARY_INSTALL_DIRECTORY}/packages/maintenancetool/data) + file(MAKE_DIRECTORY ${MT_DATA_DIR}) + execute_process( + COMMAND binarycreator --config ${CPACK_CONFIG_DIR}/cpack-config/config/config.xml --create-maintenancetool --sign ${GPT4ALL_SIGNING_ID} + WORKING_DIRECTORY ${MT_DATA_DIR} + ) +endif() diff --git a/gpt4all-chat/cmake/installer_component.qs b/gpt4all-chat/cmake/installer_gpt4all_component.qs similarity index 100% rename from gpt4all-chat/cmake/installer_component.qs rename to gpt4all-chat/cmake/installer_gpt4all_component.qs diff --git a/gpt4all-chat/cmake/installer_maintenancetool_component.qs b/gpt4all-chat/cmake/installer_maintenancetool_component.qs new file mode 100644 index 000000000000..57a9ca0eb8c5 --- /dev/null +++ b/gpt4all-chat/cmake/installer_maintenancetool_component.qs @@ -0,0 +1,19 @@ +function Component() +{ + component.ifwVersion = installer.value("FrameworkVersion"); + installer.installationStarted.connect(this, Component.prototype.onInstallationStarted); +} + +Component.prototype.onInstallationStarted = function() +{ + if (component.updateRequested() || component.installationRequested()) { + if (installer.value("os") == "win") { + component.installerbaseBinaryPath = "@TargetDir@/installerbase.exe"; + } else if (installer.value("os") == "x11") { + component.installerbaseBinaryPath = "@TargetDir@/installerbase"; + } else if (installer.value("os") == "mac") { + component.installerbaseBinaryPath = "@TargetDir@/MaintenanceTool.app"; + } + installer.setInstallerBaseBinary(component.installerbaseBinaryPath); + } +} diff --git a/gpt4all-chat/deps/CMakeLists.txt b/gpt4all-chat/deps/CMakeLists.txt index a87a9a203a4b..898cd8893c41 100644 --- a/gpt4all-chat/deps/CMakeLists.txt +++ b/gpt4all-chat/deps/CMakeLists.txt @@ -1,3 +1,6 @@ +include(FetchContent) + + set(BUILD_SHARED_LIBS OFF) set(FMT_INSTALL OFF) @@ -11,3 +14,38 @@ add_subdirectory(DuckX) set(QT_VERSION_MAJOR 6) add_subdirectory(QXlsx/QXlsx) + +if (NOT GPT4ALL_USING_QTPDF) + # If we do not use QtPDF, we need to get PDFium. + set(GPT4ALL_PDFIUM_TAG "chromium/6954") + if (CMAKE_SYSTEM_NAME MATCHES Linux) + FetchContent_Declare( + pdfium + URL "https://github.com/bblanchon/pdfium-binaries/releases/download/${GPT4ALL_PDFIUM_TAG}/pdfium-linux-x64.tgz" + URL_HASH "SHA256=69917fd9543befc6c806254aff6c8a604d9e7cd3999a3e70fc32b8690d372da2" + ) + elseif (CMAKE_SYSTEM_NAME MATCHES Windows) + if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$") + FetchContent_Declare( + pdfium + URL "https://github.com/bblanchon/pdfium-binaries/releases/download/${GPT4ALL_PDFIUM_TAG}/pdfium-win-x64.tgz" + URL_HASH "SHA256=62ecac78fbaf658457beaffcc05eb147f493d435a2e1309e6a731808b4e80d38" + ) + elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|AARCH64|arm64|ARM64)$") + FetchContent_Declare( + pdfium + URL "https://github.com/bblanchon/pdfium-binaries/releases/download/${GPT4ALL_PDFIUM_TAG}/pdfium-win-arm64.tgz" + URL_HASH "SHA256=a0b69014467f2b9824776c064920bc95359c9ba0d88793bdda1894a0f22206f8" + ) + endif() + elseif (CMAKE_SYSTEM_NAME MATCHES Darwin) + FetchContent_Declare( + pdfium + URL "https://github.com/bblanchon/pdfium-binaries/releases/download/${GPT4ALL_PDFIUM_TAG}/pdfium-mac-univ.tgz" + URL_HASH "SHA256=7442f1dc6bef90898b2b7bd38dbec369ddd81bbf66c1c5aac3a1b60e107098f9" + ) + endif() + + FetchContent_MakeAvailable(pdfium) + find_package(PDFium REQUIRED PATHS "${pdfium_SOURCE_DIR}" NO_DEFAULT_PATH) +endif() diff --git a/gpt4all-chat/deps/json b/gpt4all-chat/deps/json new file mode 160000 index 000000000000..606b6347edf0 --- /dev/null +++ b/gpt4all-chat/deps/json @@ -0,0 +1 @@ +Subproject commit 606b6347edf0758c531abb6c36743e09a4c48a84 diff --git a/gpt4all-chat/deps/minja b/gpt4all-chat/deps/minja new file mode 160000 index 000000000000..491f5cb2e992 --- /dev/null +++ b/gpt4all-chat/deps/minja @@ -0,0 +1 @@ +Subproject commit 491f5cb2e9925644bca2dddf09200042fa54bef4 diff --git a/gpt4all-chat/icons/edit.svg b/gpt4all-chat/icons/edit.svg index 5a79a50e9cf4..ceb292bbf0de 100644 --- a/gpt4all-chat/icons/edit.svg +++ b/gpt4all-chat/icons/edit.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/gpt4all-chat/main.qml b/gpt4all-chat/main.qml index b035e5d9bb29..5b60d36ae892 100644 --- a/gpt4all-chat/main.qml +++ b/gpt4all-chat/main.qml @@ -54,7 +54,7 @@ Window { systemTrayIcon.shouldClose = true; window.shouldClose = true; savingPopup.open(); - ChatListModel.saveChats(); + ChatListModel.saveChatsForQuit(); } } } @@ -231,8 +231,8 @@ Window { window.shouldClose = true; savingPopup.open(); - ChatListModel.saveChats(); - close.accepted = false + ChatListModel.saveChatsForQuit(); + close.accepted = false; } Connections { @@ -674,9 +674,6 @@ Window { function show() { stackLayout.currentIndex = 2; - // FIXME This expanded code should be removed and we should be changing the names of - // the classes here in ModelList for the proxy/filter models - ModelList.downloadableModels.expanded = true } function isShown() { diff --git a/gpt4all-chat/metadata/latestnews.md b/gpt4all-chat/metadata/latestnews.md index cfd893e00dc2..f46fb721d551 100644 --- a/gpt4all-chat/metadata/latestnews.md +++ b/gpt4all-chat/metadata/latestnews.md @@ -1,20 +1,16 @@ ## Latest News -GPT4All v3.4.1 was released on October 11th, and fixes several issues with LocalDocs from the previous release. +GPT4All v3.8.0 was released on January 30th. Changes include: -GPT4All v3.4.2 was released on October 16th, and fixes more issues with LocalDocs. - -**IMPORTANT NOTE:** If you are coming from v3.4.0, be sure to "Rebuild" your collections at least once after updating! - ---- - -GPT4All v3.4.0 was released on October 8th. Changes include: - -* **Attached Files:** You can now attach a small Microsoft Excel spreadsheet (.xlsx) to a chat message and ask the model about it. -* **LocalDocs Accuracy:** The LocalDocs algorithm has been enhanced to find more accurate references for some queries. -* **Word Document Support:** LocalDocs now supports Microsoft Word (.docx) documents natively. - * **IMPORTANT NOTE:** If .docx files are not found, make sure Settings > LocalDocs > Allowed File Extensions includes "docx". -* **Forgetful Model Fixes:** Issues with the "Redo last chat response" button, and with continuing chats from previous sessions, have been fixed. -* **Chat Saving Improvements:** On exit, GPT4All will no longer save chats that are not new or modified. As a bonus, downgrading without losing access to all chats will be possible in the future, should the need arise. -* **UI Fixes:** The model list no longer scrolls to the top when you start downloading a model. -* **New Models:** LLama 3.2 Instruct 3B and 1B models now available in model list. +* **Native DeepSeek-R1-Distill Support:** GPT4All now has robust support for the DeepSeek-R1 family of distillations. + * Several model variants are now available on the downloads page. + * Reasoning (wrapped in "think" tags) is displayed similarly to the Reasoner model. + * The DeepSeek-R1 Qwen pretokenizer is now supported, resolving the loading failure in previous versions. + * The model is now configured with a GPT4All-compatible prompt template by default. +* **Chat Templating Overhaul:** The template parser has been *completely* replaced with one that has much better compatibility with common models. +* **Code Interpreter Fixes:** + * An issue preventing the code interpreter from logging a single string in v3.7.0 has been fixed. + * The UI no longer freezes while the code interpreter is running a computation. +* **Local Server Fixes:** + * An issue preventing the server from using LocalDocs after the first request since v3.5.0 has been fixed. + * System messages are now correctly hidden from the message history. diff --git a/gpt4all-chat/metadata/models3.json b/gpt4all-chat/metadata/models3.json index e4a2162f3317..654fe18d4610 100644 --- a/gpt4all-chat/metadata/models3.json +++ b/gpt4all-chat/metadata/models3.json @@ -1,6 +1,22 @@ [ { "order": "a", + "md5sum": "a54c08a7b90e4029a8c2ab5b5dc936aa", + "name": "Reasoner v1", + "filename": "qwen2.5-coder-7b-instruct-q4_0.gguf", + "filesize": "4431390720", + "requires": "3.6.0", + "ramrequired": "8", + "parameters": "8 billion", + "quant": "q4_0", + "type": "qwen2", + "description": "", + "url": "https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct-GGUF/resolve/main/qwen2.5-coder-7b-instruct-q4_0.gguf", + "chatTemplate": "{{- '<|im_start|>system\\n' }}\n{% if toolList|length > 0 %}You have access to the following functions:\n{% for tool in toolList %}\nUse the function '{{tool.function}}' to: '{{tool.description}}'\n{% if tool.parameters|length > 0 %}\nparameters:\n{% for info in tool.parameters %}\n {{info.name}}:\n type: {{info.type}}\n description: {{info.description}}\n required: {{info.required}}\n{% endfor %}\n{% endif %}\n# Tool Instructions\nIf you CHOOSE to call this function ONLY reply with the following format:\n'{{tool.symbolicFormat}}'\nHere is an example. If the user says, '{{tool.examplePrompt}}', then you reply\n'{{tool.exampleCall}}'\nAfter the result you might reply with, '{{tool.exampleReply}}'\n{% endfor %}\nYou MUST include both the start and end tags when you use a function.\n\nYou are a helpful AI assistant who uses the functions to break down, analyze, perform, and verify complex reasoning tasks. You SHOULD try to verify your answers using the functions where possible.\n{% endif %}\n{{- '<|im_end|>\\n' }}\n{% for message in messages %}\n{{'<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>\\n' }}\n{% endfor %}\n{% if add_generation_prompt %}\n{{ '<|im_start|>assistant\\n' }}\n{% endif %}\n", + "systemPrompt": "" + }, + { + "order": "aa", "md5sum": "c87ad09e1e4c8f9c35a5fcef52b6f1c9", "name": "Llama 3 8B Instruct", "filename": "Meta-Llama-3-8B-Instruct.Q4_0.gguf", @@ -13,7 +29,68 @@ "description": "", "url": "https://gpt4all.io/models/gguf/Meta-Llama-3-8B-Instruct.Q4_0.gguf", "promptTemplate": "<|start_header_id|>user<|end_header_id|>\n\n%1<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n%2<|eot_id|>", - "systemPrompt": "" + "systemPrompt": "", + "chatTemplate": "{%- set loop_messages = messages %}\n{%- for message in loop_messages %}\n {%- set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\\n\\n'+ message['content'] | trim + '<|eot_id|>' %}\n {{- content }}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|start_header_id|>assistant<|end_header_id|>\\n\\n' }}\n{%- endif %}" + }, + { + "order": "aa1", + "sha256sum": "5cd4ee65211770f1d99b4f6f4951780b9ef40e29314bd6542bb5bd0ad0bc29d1", + "name": "DeepSeek-R1-Distill-Qwen-7B", + "filename": "DeepSeek-R1-Distill-Qwen-7B-Q4_0.gguf", + "filesize": "4444121056", + "requires": "3.8.0", + "ramrequired": "8", + "parameters": "7 billion", + "quant": "q4_0", + "type": "deepseek", + "description": "

The official Qwen2.5-Math-7B distillation of DeepSeek-R1.

  • License: MIT
  • No restrictions on commercial use
  • #reasoning
", + "url": "https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-7B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-7B-Q4_0.gguf", + "chatTemplate": "{%- if not add_generation_prompt is defined %}\n {%- set add_generation_prompt = false %}\n{%- endif %}\n{%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n{%- endif %}\n{%- for message in messages %}\n {%- if message['role'] == 'user' %}\n {{- '<|User|>' + message['content'] }}\n {%- endif %}\n {%- if message['role'] == 'assistant' %}\n {%- set content = message['content'] | regex_replace('^[\\\\s\\\\S]*', '') %}\n {{- '<|Assistant|>' + content + '<|end▁of▁sentence|>' }}\n {%- endif %}\n{%- endfor -%}\n{%- if add_generation_prompt %}\n {{- '<|Assistant|>' }}\n{%- endif %}" + }, + { + "order": "aa2", + "sha256sum": "906b3382f2680f4ce845459b4a122e904002b075238080307586bcffcde49eef", + "name": "DeepSeek-R1-Distill-Qwen-14B", + "filename": "DeepSeek-R1-Distill-Qwen-14B-Q4_0.gguf", + "filesize": "8544267680", + "requires": "3.8.0", + "ramrequired": "16", + "parameters": "14 billion", + "quant": "q4_0", + "type": "deepseek", + "description": "

The official Qwen2.5-14B distillation of DeepSeek-R1.

  • License: MIT
  • No restrictions on commercial use
  • #reasoning
", + "url": "https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-14B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-14B-Q4_0.gguf", + "chatTemplate": "{%- if not add_generation_prompt is defined %}\n {%- set add_generation_prompt = false %}\n{%- endif %}\n{%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n{%- endif %}\n{%- for message in messages %}\n {%- if message['role'] == 'user' %}\n {{- '<|User|>' + message['content'] }}\n {%- endif %}\n {%- if message['role'] == 'assistant' %}\n {%- set content = message['content'] | regex_replace('^[\\\\s\\\\S]*', '') %}\n {{- '<|Assistant|>' + content + '<|end▁of▁sentence|>' }}\n {%- endif %}\n{%- endfor -%}\n{%- if add_generation_prompt %}\n {{- '<|Assistant|>' }}\n{%- endif %}" + }, + { + "order": "aa3", + "sha256sum": "0eb93e436ac8beec18aceb958c120d282cb2cf5451b23185e7be268fe9d375cc", + "name": "DeepSeek-R1-Distill-Llama-8B", + "filename": "DeepSeek-R1-Distill-Llama-8B-Q4_0.gguf", + "filesize": "4675894112", + "requires": "3.8.0", + "ramrequired": "8", + "parameters": "8 billion", + "quant": "q4_0", + "type": "deepseek", + "description": "

The official Llama-3.1-8B distillation of DeepSeek-R1.

  • License: MIT
  • No restrictions on commercial use
  • #reasoning
", + "url": "https://huggingface.co/bartowski/DeepSeek-R1-Distill-Llama-8B-GGUF/resolve/main/DeepSeek-R1-Distill-Llama-8B-Q4_0.gguf", + "chatTemplate": "{%- if not add_generation_prompt is defined %}\n {%- set add_generation_prompt = false %}\n{%- endif %}\n{%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n{%- endif %}\n{%- for message in messages %}\n {%- if message['role'] == 'user' %}\n {{- '<|User|>' + message['content'] }}\n {%- endif %}\n {%- if message['role'] == 'assistant' %}\n {%- set content = message['content'] | regex_replace('^[\\\\s\\\\S]*', '') %}\n {{- '<|Assistant|>' + content + '<|end▁of▁sentence|>' }}\n {%- endif %}\n{%- endfor -%}\n{%- if add_generation_prompt %}\n {{- '<|Assistant|>' }}\n{%- endif %}" + }, + { + "order": "aa4", + "sha256sum": "b3af887d0a015b39fab2395e4faf682c1a81a6a3fd09a43f0d4292f7d94bf4d0", + "name": "DeepSeek-R1-Distill-Qwen-1.5B", + "filename": "DeepSeek-R1-Distill-Qwen-1.5B-Q4_0.gguf", + "filesize": "1068807776", + "requires": "3.8.0", + "ramrequired": "3", + "parameters": "1.5 billion", + "quant": "q4_0", + "type": "deepseek", + "description": "

The official Qwen2.5-Math-1.5B distillation of DeepSeek-R1.

  • License: MIT
  • No restrictions on commercial use
  • #reasoning
", + "url": "https://huggingface.co/bartowski/DeepSeek-R1-Distill-Qwen-1.5B-GGUF/resolve/main/DeepSeek-R1-Distill-Qwen-1.5B-Q4_0.gguf", + "chatTemplate": "{%- if not add_generation_prompt is defined %}\n {%- set add_generation_prompt = false %}\n{%- endif %}\n{%- if messages[0]['role'] == 'system' %}\n {{- messages[0]['content'] }}\n{%- endif %}\n{%- for message in messages %}\n {%- if message['role'] == 'user' %}\n {{- '<|User|>' + message['content'] }}\n {%- endif %}\n {%- if message['role'] == 'assistant' %}\n {%- set content = message['content'] | regex_replace('^[\\\\s\\\\S]*', '') %}\n {{- '<|Assistant|>' + content + '<|end▁of▁sentence|>' }}\n {%- endif %}\n{%- endfor -%}\n{%- if add_generation_prompt %}\n {{- '<|Assistant|>' }}\n{%- endif %}" }, { "order": "b", @@ -29,7 +106,8 @@ "description": "", "url": "https://huggingface.co/bartowski/Llama-3.2-3B-Instruct-GGUF/resolve/main/Llama-3.2-3B-Instruct-Q4_0.gguf", "promptTemplate": "<|start_header_id|>user<|end_header_id|>\n\n%1<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n%2", - "systemPrompt": "<|start_header_id|>system<|end_header_id|>\nCutting Knowledge Date: December 2023\n\nYou are a helpful assistant.<|eot_id|>" + "systemPrompt": "<|start_header_id|>system<|end_header_id|>\nCutting Knowledge Date: December 2023\n\nYou are a helpful assistant.<|eot_id|>", + "chatTemplate": "{{- bos_token }}\n{%- set date_string = strftime_now('%d %b %Y') %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0]['role'] == 'system' %}\n {%- set system_message = messages[0]['content'] | trim %}\n {%- set loop_start = 1 %}\n{%- else %}\n {%- set system_message = '' %}\n {%- set loop_start = 0 %}\n{%- endif %}\n\n{#- System message #}\n{{- '<|start_header_id|>system<|end_header_id|>\\n\\n' }}\n{{- 'Cutting Knowledge Date: December 2023\\n' }}\n{{- 'Today Date: ' + date_string + '\\n\\n' }}\n{{- system_message }}\n{{- '<|eot_id|>' }}\n\n{%- for message in messages %}\n {%- if loop.index0 >= loop_start %}\n {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\\n\\n' + message['content'] | trim + '<|eot_id|>' }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|start_header_id|>assistant<|end_header_id|>\\n\\n' }}\n{%- endif %}" }, { "order": "c", @@ -45,7 +123,8 @@ "description": "", "url": "https://huggingface.co/bartowski/Llama-3.2-1B-Instruct-GGUF/resolve/main/Llama-3.2-1B-Instruct-Q4_0.gguf", "promptTemplate": "<|start_header_id|>user<|end_header_id|>\n\n%1<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n%2", - "systemPrompt": "<|start_header_id|>system<|end_header_id|>\nCutting Knowledge Date: December 2023\n\nYou are a helpful assistant.<|eot_id|>" + "systemPrompt": "<|start_header_id|>system<|end_header_id|>\nCutting Knowledge Date: December 2023\n\nYou are a helpful assistant.<|eot_id|>", + "chatTemplate": "{{- bos_token }}\n{%- set date_string = strftime_now('%d %b %Y') %}\n\n{#- This block extracts the system message, so we can slot it into the right place. #}\n{%- if messages[0]['role'] == 'system' %}\n {%- set system_message = messages[0]['content'] | trim %}\n {%- set loop_start = 1 %}\n{%- else %}\n {%- set system_message = '' %}\n {%- set loop_start = 0 %}\n{%- endif %}\n\n{#- System message #}\n{{- '<|start_header_id|>system<|end_header_id|>\\n\\n' }}\n{{- 'Cutting Knowledge Date: December 2023\\n' }}\n{{- 'Today Date: ' + date_string + '\\n\\n' }}\n{{- system_message }}\n{{- '<|eot_id|>' }}\n\n{%- for message in messages %}\n {%- if loop.index0 >= loop_start %}\n {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\\n\\n' + message['content'] | trim + '<|eot_id|>' }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|start_header_id|>assistant<|end_header_id|>\\n\\n' }}\n{%- endif %}" }, { "order": "d", @@ -61,7 +140,8 @@ "description": "Good overall fast chat model
  • Fast responses
  • Chat based model
  • Accepts system prompts in ChatML format
  • Trained by Mistral AI
  • Finetuned by Nous Research on the OpenHermes-2.5 dataset
  • Licensed for commercial use
", "url": "https://huggingface.co/NousResearch/Nous-Hermes-2-Mistral-7B-DPO-GGUF/resolve/main/Nous-Hermes-2-Mistral-7B-DPO.Q4_0.gguf", "promptTemplate": "<|im_start|>user\n%1<|im_end|>\n<|im_start|>assistant\n%2<|im_end|>\n", - "systemPrompt": "" + "systemPrompt": "", + "chatTemplate": "{%- for message in messages %}\n {{- '<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>\\n' }}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}" }, { "order": "e", @@ -77,7 +157,8 @@ "systemPrompt": "", "description": "Strong overall fast instruction following model
  • Fast responses
  • Trained by Mistral AI
  • Uncensored
  • Licensed for commercial use
", "url": "https://gpt4all.io/models/gguf/mistral-7b-instruct-v0.1.Q4_0.gguf", - "promptTemplate": "[INST] %1 [/INST]" + "promptTemplate": "[INST] %1 [/INST]", + "chatTemplate": "{%- if messages[0]['role'] == 'system' %}\n {%- set system_message = messages[0]['content'] %}\n {%- set loop_start = 1 %}\n{%- else %}\n {%- set loop_start = 0 %}\n{%- endif %}\n{%- for message in messages %}\n {%- if loop.index0 >= loop_start %}\n {%- if (message['role'] == 'user') != ((loop.index0 - loop_start) % 2 == 0) %}\n {{- raise_exception('After the optional system message, conversation roles must alternate user/assistant/user/assistant/...') }}\n {%- endif %}\n {%- if message['role'] == 'user' %}\n {%- if loop.index0 == loop_start and loop_start == 1 %}\n {{- ' [INST] ' + system_message + '\\n\\n' + message['content'] + ' [/INST]' }}\n {%- else %}\n {{- ' [INST] ' + message['content'] + ' [/INST]' }}\n {%- endif %}\n {%- elif message['role'] == 'assistant' %}\n {{- ' ' + message['content'] + eos_token }}\n {%- else %}\n {{- raise_exception('Only user and assistant roles are supported, with the exception of an initial optional system message!') }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}" }, { "order": "f", @@ -93,7 +174,8 @@ "description": "
  • For advanced users only. Not recommended for use on Windows or Linux without selecting CUDA due to speed issues.
  • Fast responses
  • Chat based model
  • Large context size of 128k
  • Accepts agentic system prompts in Llama 3.1 format
  • Trained by Meta
  • License: Meta Llama 3.1 Community License
", "url": "https://huggingface.co/GPT4All-Community/Meta-Llama-3.1-8B-Instruct-128k/resolve/main/Meta-Llama-3.1-8B-Instruct-128k-Q4_0.gguf", "promptTemplate": "<|start_header_id|>user<|end_header_id|>\n\n%1<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n%2", - "systemPrompt": "<|start_header_id|>system<|end_header_id|>\nCutting Knowledge Date: December 2023\n\nYou are a helpful assistant.<|eot_id|>" + "systemPrompt": "<|start_header_id|>system<|end_header_id|>\nCutting Knowledge Date: December 2023\n\nYou are a helpful assistant.<|eot_id|>", + "chatTemplate": "{%- set loop_messages = messages %}\n{%- for message in loop_messages %}\n {%- set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\\n\\n'+ message['content'] | trim + '<|eot_id|>' %}\n {%- if loop.index0 == 0 %}\n {%- set content = bos_token + content %}\n {%- endif %}\n {{- content }}\n{%- endfor %}\n{{- '<|start_header_id|>assistant<|end_header_id|>\\n\\n' }}" }, { "order": "g", @@ -109,7 +191,8 @@ "description": "Strong overall fast chat model
  • Fast responses
  • Chat based model
  • Trained by Mistral AI
  • Finetuned on OpenOrca dataset curated via Nomic Atlas
  • Licensed for commercial use
", "url": "https://gpt4all.io/models/gguf/mistral-7b-openorca.gguf2.Q4_0.gguf", "promptTemplate": "<|im_start|>user\n%1<|im_end|>\n<|im_start|>assistant\n%2<|im_end|>\n", - "systemPrompt": "<|im_start|>system\nYou are MistralOrca, a large language model trained by Alignment Lab AI.\n<|im_end|>\n" + "systemPrompt": "<|im_start|>system\nYou are MistralOrca, a large language model trained by Alignment Lab AI.\n<|im_end|>\n", + "chatTemplate": "{%- for message in messages %}\n {{- '<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>\\n' }}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}" }, { "order": "h", @@ -125,7 +208,8 @@ "systemPrompt": "", "description": "Very fast model with good quality
  • Fastest responses
  • Instruction based
  • Trained by TII
  • Finetuned by Nomic AI
  • Licensed for commercial use
", "url": "https://gpt4all.io/models/gguf/gpt4all-falcon-newbpe-q4_0.gguf", - "promptTemplate": "### Instruction:\n%1\n\n### Response:\n" + "promptTemplate": "### Instruction:\n%1\n\n### Response:\n", + "chatTemplate": "{%- if messages[0]['role'] == 'system' %}\n {%- set loop_start = 1 %}\n {{- messages[0]['content'] + '\\n\\n' }}\n{%- else %}\n {%- set loop_start = 0 %}\n{%- endif %}\n{%- for message in messages %}\n {%- if loop.index0 >= loop_start %}\n {%- if message['role'] == 'user' %}\n {{- '### User: ' + message['content'] + '\\n\\n' }}\n {%- elif message['role'] == 'assistant' %}\n {{- '### Assistant: ' + message['content'] + '\\n\\n' }}\n {%- else %}\n {{- raise_exception('Only user and assistant roles are supported, with the exception of an initial optional system message!') }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '### Assistant:' }}\n{%- endif %}" }, { "order": "i", @@ -140,7 +224,8 @@ "type": "LLaMA2", "systemPrompt": "", "description": "
  • Instruction based
  • Trained by Microsoft
  • Cannot be used commercially
", - "url": "https://gpt4all.io/models/gguf/orca-2-7b.Q4_0.gguf" + "url": "https://gpt4all.io/models/gguf/orca-2-7b.Q4_0.gguf", + "chatTemplate": "{%- for message in messages %}\n {{- '<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>\\n' }}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}" }, { "order": "j", @@ -155,7 +240,8 @@ "type": "LLaMA2", "systemPrompt": "", "description": "
  • Instruction based
  • Trained by Microsoft
  • Cannot be used commercially
", - "url": "https://gpt4all.io/models/gguf/orca-2-13b.Q4_0.gguf" + "url": "https://gpt4all.io/models/gguf/orca-2-13b.Q4_0.gguf", + "chatTemplate": "{%- for message in messages %}\n {{- '<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>\\n' }}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}" }, { "order": "k", @@ -170,7 +256,9 @@ "type": "LLaMA2", "systemPrompt": "", "description": "Strong overall larger model
  • Instruction based
  • Gives very long responses
  • Finetuned with only 1k of high-quality data
  • Trained by Microsoft and Peking University
  • Cannot be used commercially
", - "url": "https://gpt4all.io/models/gguf/wizardlm-13b-v1.2.Q4_0.gguf" + "url": "https://gpt4all.io/models/gguf/wizardlm-13b-v1.2.Q4_0.gguf", + "chatTemplate": "{%- if messages[0]['role'] == 'system' %}\n {%- set loop_start = 1 %}\n {{- messages[0]['content'] + ' ' }}\n{%- else %}\n {%- set loop_start = 0 %}\n{%- endif %}\n{%- for message in loop_messages %}\n {%- if loop.index0 >= loop_start %}\n {%- if message['role'] == 'user' %}\n {{- 'USER: ' + message['content'] }}\n {%- elif message['role'] == 'assistant' %}\n {{- 'ASSISTANT: ' + message['content'] }}\n {%- else %}\n {{- raise_exception('Only user and assistant roles are supported, with the exception of an initial optional system message!') }}\n {%- endif %}\n {%- if (loop.index0 - loop_start) % 2 == 0 %}\n {{- ' ' }}\n {%- else %}\n {{- eos_token }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- 'ASSISTANT:' }}\n{%- endif %}", + "systemMessage": "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions." }, { "order": "l", @@ -186,7 +274,9 @@ "description": "Ghost 7B v0.9.1 fast, powerful and smooth for Vietnamese and English languages.", "url": "https://huggingface.co/lamhieu/ghost-7b-v0.9.1-gguf/resolve/main/ghost-7b-v0.9.1-Q4_0.gguf", "promptTemplate": "<|user|>\n%1\n<|assistant|>\n%2\n", - "systemPrompt": "<|system|>\nYou are Ghost created by Lam Hieu. You are a helpful and knowledgeable assistant. You like to help and always give honest information, in its original language. In communication, you are always respectful, equal and promote positive behavior.\n" + "systemPrompt": "<|system|>\nYou are Ghost created by Lam Hieu. You are a helpful and knowledgeable assistant. You like to help and always give honest information, in its original language. In communication, you are always respectful, equal and promote positive behavior.\n", + "chatTemplate": "{%- for message in messages %}\n {%- if message['role'] == 'user' %}\n {{- '<|user|>\\n' + message['content'] + eos_token }}\n {%- elif message['role'] == 'system' %}\n {{- '<|system|>\\n' + message['content'] + eos_token }}\n {%- elif message['role'] == 'assistant' %}\n {{- '<|assistant|>\\n' + message['content'] + eos_token }}\n {%- endif %}\n {%- if loop.last and add_generation_prompt %}\n {{- '<|assistant|>' }}\n {%- endif %}\n{%- endfor %}", + "systemMessage": "You are Ghost created by Lam Hieu. You are a helpful and knowledgeable assistant. You like to help and always give honest information, in its original language. In communication, you are always respectful, equal and promote positive behavior." }, { "order": "m", @@ -202,7 +292,8 @@ "systemPrompt": "", "description": "Extremely good model
  • Instruction based
  • Gives long responses
  • Curated with 300,000 uncensored instructions
  • Trained by Nous Research
  • Cannot be used commercially
", "url": "https://gpt4all.io/models/gguf/nous-hermes-llama2-13b.Q4_0.gguf", - "promptTemplate": "### Instruction:\n%1\n\n### Response:\n" + "promptTemplate": "### Instruction:\n%1\n\n### Response:\n", + "chatTemplate": "{%- if messages[0]['role'] == 'system' %}\n {%- set loop_start = 1 %}\n {{- messages[0]['content'] + '\\n\\n' }}\n{%- else %}\n {%- set loop_start = 0 %}\n{%- endif %}\n{%- for message in messages %}\n {%- if loop.index0 >= loop_start %}\n {%- if message['role'] == 'user' %}\n {{- '### Instruction:\\n' + message['content'] + '\\n\\n' }}\n {%- elif message['role'] == 'assistant' %}\n {{- '### Response:\\n' + message['content'] + '\\n\\n' }}\n {%- else %}\n {{- raise_exception('Only user and assistant roles are supported, with the exception of an initial optional system message!') }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '### Instruction:\\n' }}\n{%- endif %}" }, { "order": "n", @@ -217,7 +308,9 @@ "type": "LLaMA", "systemPrompt": "", "description": "Very good overall model
  • Instruction based
  • Based on the same dataset as Groovy
  • Slower than Groovy, with higher quality responses
  • Trained by Nomic AI
  • Cannot be used commercially
", - "url": "https://gpt4all.io/models/gguf/gpt4all-13b-snoozy-q4_0.gguf" + "url": "https://gpt4all.io/models/gguf/gpt4all-13b-snoozy-q4_0.gguf", + "chatTemplate": "{%- if messages[0]['role'] == 'system' %}\n {%- set loop_start = 1 %}\n {{- messages[0]['content'] + '\\n\\n' }}\n{%- else %}\n {%- set loop_start = 0 %}\n{%- endif %}\n{%- for message in messages %}\n {%- if loop.index0 >= loop_start %}\n {%- if message['role'] == 'user' %}\n {{- '### Instruction:\\n' + message['content'] + '\\n\\n' }}\n {%- elif message['role'] == 'assistant' %}\n {{- '### Response:\\n' + message['content'] + '\\n\\n' }}\n {%- else %}\n {{- raise_exception('Only user and assistant roles are supported, with the exception of an initial optional system message!') }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '### Response:\\n' }}\n{%- endif %}", + "systemMessage": "Below is an instruction that describes a task. Write a response that appropriately completes the request." }, { "order": "o", @@ -234,7 +327,8 @@ "description": "Good model with novel architecture
  • Fast responses
  • Chat based
  • Trained by Mosaic ML
  • Cannot be used commercially
", "url": "https://gpt4all.io/models/gguf/mpt-7b-chat-newbpe-q4_0.gguf", "promptTemplate": "<|im_start|>user\n%1<|im_end|>\n<|im_start|>assistant\n%2<|im_end|>\n", - "systemPrompt": "<|im_start|>system\n- You are a helpful assistant chatbot trained by MosaicML.\n- You answer questions.\n- You are excited to be able to help the user, but will refuse to do anything that could be considered harmful to the user.\n- You are more than just an information source, you are also able to write poetry, short stories, and make jokes.<|im_end|>\n" + "systemPrompt": "<|im_start|>system\n- You are a helpful assistant chatbot trained by MosaicML.\n- You answer questions.\n- You are excited to be able to help the user, but will refuse to do anything that could be considered harmful to the user.\n- You are more than just an information source, you are also able to write poetry, short stories, and make jokes.<|im_end|>\n", + "chatTemplate": "{%- for message in messages %}\n {{- '<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>\\n' }}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}" }, { "order": "p", @@ -250,7 +344,8 @@ "description": "Good model with novel architecture
  • Fast responses
  • Chat based
  • Trained by Mosaic ML
  • Cannot be used commercially
", "url": "https://gpt4all.io/models/gguf/mpt-7b-chat.gguf4.Q4_0.gguf", "promptTemplate": "<|im_start|>user\n%1<|im_end|>\n<|im_start|>assistant\n%2<|im_end|>\n", - "systemPrompt": "<|im_start|>system\n- You are a helpful assistant chatbot trained by MosaicML.\n- You answer questions.\n- You are excited to be able to help the user, but will refuse to do anything that could be considered harmful to the user.\n- You are more than just an information source, you are also able to write poetry, short stories, and make jokes.<|im_end|>\n" + "systemPrompt": "<|im_start|>system\n- You are a helpful assistant chatbot trained by MosaicML.\n- You answer questions.\n- You are excited to be able to help the user, but will refuse to do anything that could be considered harmful to the user.\n- You are more than just an information source, you are also able to write poetry, short stories, and make jokes.<|im_end|>\n", + "chatTemplate": "{%- for message in messages %}\n {{- '<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>\\n' }}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}" }, { "order": "q", @@ -266,7 +361,8 @@ "description": "
  • Very fast responses
  • Chat based model
  • Accepts system prompts in Phi-3 format
  • Trained by Microsoft
  • License: MIT
  • No restrictions on commercial use
", "url": "https://gpt4all.io/models/gguf/Phi-3-mini-4k-instruct.Q4_0.gguf", "promptTemplate": "<|user|>\n%1<|end|>\n<|assistant|>\n%2<|end|>\n", - "systemPrompt": "" + "systemPrompt": "", + "chatTemplate": "{{- bos_token }}\n{%- for message in messages %}\n {{- '<|' + message['role'] + '|>\\n' + message['content'] + '<|end|>\\n' }}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|assistant|>\\n' }}\n{%- else %}\n {{- eos_token }}\n{%- endif %}" }, { "order": "r", @@ -282,7 +378,8 @@ "description": "Small version of new model with novel dataset
  • Very fast responses
  • Instruction based
  • Explain tuned datasets
  • Orca Research Paper dataset construction approaches
  • Cannot be used commercially
", "url": "https://gpt4all.io/models/gguf/orca-mini-3b-gguf2-q4_0.gguf", "promptTemplate": "### User:\n%1\n\n### Response:\n", - "systemPrompt": "### System:\nYou are an AI assistant that follows instruction extremely well. Help as much as you can.\n\n" + "systemPrompt": "### System:\nYou are an AI assistant that follows instruction extremely well. Help as much as you can.\n\n", + "chatTemplate": "{%- if messages[0]['role'] == 'system' %}\n {%- set loop_start = 1 %}\n {{- '### System:\\n' + messages[0]['content'] + '\\n\\n' }}\n{%- else %}\n {%- set loop_start = 0 %}\n{%- endif %}\n{%- for message in messages %}\n {%- if loop.index0 >= loop_start %}\n {%- if message['role'] == 'user' %}\n {{- '### User:\\n' + message['content'] + '\\n\\n' }}\n {%- elif message['role'] == 'assistant' %}\n {{- '### Response:\\n' + message['content'] + '\\n\\n' }}\n {%- else %}\n {{- raise_exception('Only user and assistant roles are supported, with the exception of an initial optional system message!') }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '### Response:\\n' }}\n{%- endif %}" }, { "order": "s", @@ -299,7 +396,8 @@ "systemPrompt": "", "promptTemplate": "%1", "description": "Trained on subset of the Stack
  • Code completion based
  • Licensed for commercial use
  • WARNING: Not available for chat GUI
", - "url": "https://gpt4all.io/models/gguf/replit-code-v1_5-3b-newbpe-q4_0.gguf" + "url": "https://gpt4all.io/models/gguf/replit-code-v1_5-3b-newbpe-q4_0.gguf", + "chatTemplate": null }, { "order": "t", @@ -316,7 +414,8 @@ "systemPrompt": "", "promptTemplate": "%1", "description": "Trained on subset of the Stack
  • Code completion based
  • WARNING: Not available for chat GUI
", - "url": "https://gpt4all.io/models/gguf/starcoder-newbpe-q4_0.gguf" + "url": "https://gpt4all.io/models/gguf/starcoder-newbpe-q4_0.gguf", + "chatTemplate": null }, { "order": "u", @@ -333,7 +432,8 @@ "systemPrompt": "", "promptTemplate": "%1", "description": "Trained on collection of Python and TypeScript
  • Code completion based
  • WARNING: Not available for chat GUI
  • ", - "url": "https://gpt4all.io/models/gguf/rift-coder-v0-7b-q4_0.gguf" + "url": "https://gpt4all.io/models/gguf/rift-coder-v0-7b-q4_0.gguf", + "chatTemplate": null }, { "order": "v", @@ -351,7 +451,8 @@ "embeddingModel": true, "systemPrompt": "", "description": "LocalDocs text embeddings model
    • For use with LocalDocs feature
    • Used for retrieval augmented generation (RAG)", - "url": "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2-f16.gguf" + "url": "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2-f16.gguf", + "chatTemplate": null }, { "order": "w", @@ -367,7 +468,8 @@ "type": "Bert", "embeddingModel": true, "description": "LocalDocs text embeddings model
      • For use with LocalDocs feature
      • Used for retrieval augmented generation (RAG)", - "url": "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2.gguf2.f16.gguf" + "url": "https://gpt4all.io/models/gguf/all-MiniLM-L6-v2.gguf2.f16.gguf", + "chatTemplate": null }, { "order": "x", @@ -383,7 +485,9 @@ "description": "Mistral-based model for German-language applications
        • Fast responses
        • Chat based model
        • Trained by ellamind
        • Finetuned on German instruction and chat data
        • Licensed for commercial use
        ", "url": "https://huggingface.co/TheBloke/em_german_mistral_v01-GGUF/resolve/main/em_german_mistral_v01.Q4_0.gguf", "promptTemplate": "USER: %1 ASSISTANT: ", - "systemPrompt": "Du bist ein hilfreicher Assistent. " + "systemPrompt": "Du bist ein hilfreicher Assistent. ", + "chatTemplate": "{%- if messages[0]['role'] == 'system' %}\n {%- set loop_start = 1 %}\n {{- messages[0]['content'] }}\n{%- else %}\n {%- set loop_start = 0 %}\n{%- endif %}\n{%- for message in messages %}\n {%- if loop.index0 >= loop_start %}\n {%- if not loop.first %}\n {{- ' ' }}\n {%- endif %}\n {%- if message['role'] == 'user' %}\n {{- 'USER: ' + message['content'] }}\n {%- elif message['role'] == 'assistant' %}\n {{- 'ASSISTANT: ' + message['content'] }}\n {%- else %}\n {{- raise_exception('After the optional system message, conversation roles must be either user or assistant.') }}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {%- if messages %}\n {{- ' ' }}\n {%- endif %}\n {{- 'ASSISTANT:' }}\n{%- endif %}", + "systemMessage": "Du bist ein hilfreicher Assistent." }, { "order": "y", @@ -400,7 +504,8 @@ "embeddingModel": true, "systemPrompt": "", "description": "nomic-embed-text-v1", - "url": "https://gpt4all.io/models/gguf/nomic-embed-text-v1.f16.gguf" + "url": "https://gpt4all.io/models/gguf/nomic-embed-text-v1.f16.gguf", + "chatTemplate": null }, { "order": "z", @@ -417,7 +522,8 @@ "embeddingModel": true, "systemPrompt": "", "description": "nomic-embed-text-v1.5", - "url": "https://gpt4all.io/models/gguf/nomic-embed-text-v1.5.f16.gguf" + "url": "https://gpt4all.io/models/gguf/nomic-embed-text-v1.5.f16.gguf", + "chatTemplate": null }, { "order": "zzz", @@ -426,13 +532,14 @@ "filename": "qwen2-1_5b-instruct-q4_0.gguf", "filesize": "937532800", "requires": "3.0", - "ramrequired": "4", + "ramrequired": "3", "parameters": "1.5 billion", "quant": "q4_0", "type": "qwen2", "description": "
        • Very fast responses
        • Instruction based model
        • Usage of LocalDocs (RAG): Highly recommended
        • Supports context length of up to 32768
        • Trained and finetuned by Qwen (Alibaba Cloud)
        • License: Apache 2.0
        ", "url": "https://huggingface.co/Qwen/Qwen2-1.5B-Instruct-GGUF/resolve/main/qwen2-1_5b-instruct-q4_0.gguf", "promptTemplate": "<|im_start|>user\n%1<|im_end|>\n<|im_start|>assistant\n%2<|im_end|>", - "systemPrompt": "<|im_start|>system\nBelow is an instruction that describes a task. Write a response that appropriately completes the request.<|im_end|>\n" + "systemPrompt": "<|im_start|>system\nBelow is an instruction that describes a task. Write a response that appropriately completes the request.<|im_end|>\n", + "chatTemplate": "{%- for message in messages %}\n {%- if loop.first and messages[0]['role'] != 'system' %}\n {{- '<|im_start|>system\\nYou are a helpful assistant.<|im_end|>\\n' }}\n {%- endif %}\n {{- '<|im_start|>' + message['role'] + '\\n' + message['content'] + '<|im_end|>\\n' }}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n{%- endif %}" } ] diff --git a/gpt4all-chat/metadata/release.json b/gpt4all-chat/metadata/release.json index 36e1de871b5e..70d1595e1b72 100644 --- a/gpt4all-chat/metadata/release.json +++ b/gpt4all-chat/metadata/release.json @@ -228,5 +228,45 @@ "version": "3.4.2", "notes": "* **LocalDocs Fixes:** Several issues with LocalDocs, some of which were introduced in v3.4.0, have been fixed.\n * Fixed the possible use of references from unselected collections.\n * Fixed unnecessary reindexing of files with uppercase extensions.\n * Fixed hybrid search failure due to inconsistent database state.\n * Fully fixed the blank Embeddings Device selection in LocalDocs settings.\n * Fixed LocalDocs indexing of large PDFs making very slow progress or even stalling.\n", "contributors": "* Adam Treat (Nomic AI)\n* Jared Van Bortel (Nomic AI)" + }, + { + "version": "3.5.0", + "notes": "* **Message Editing:**\n * You can now edit any message you've sent by clicking the pencil icon below it.\n * You can now redo earlier responses in the conversation.\n* **Templates:** Chat templates have been completely overhauled! They now use Jinja-style syntax. You may notice warnings or errors in the UI. Read the linked docs, and if you have any questions, please ask on the Discord.\n* **File Attachments:** Markdown and plain text files are now supported as file attachments.\n* **System Tray:** There is now an option in Application Settings to allow GPT4All to minimize to the system tray instead of closing.\n* **Local API Server:**\n * The API server now supports system messages from the client and no longer uses the system message in settings.\n * You can now send messages to the API server in any order supported by the model instead of just user/assistant pairs.\n* **Translations:** The Italian and Romanian translations have been improved.\n", + "contributors": "* Jared Van Bortel (Nomic AI)\n* Adam Treat (Nomic AI)\n* Benjamin Gallois (`@bgallois`)\n* Riccardo Giovanetti (`@Harvester62`)\n* Victor Emanuel (`@SINAPSA-IC`)" + }, + { + "version": "3.5.1", + "notes": "* **Chat template fixes:** Llama 3.2 models, Nous Hermes 2 Mistral, Mistral OpenOrca, Qwen 2 and remote models\n* **Bugfix:** Fix the default model button so it works again after 3.5.0\n", + "contributors": "* Jared Van Bortel (Nomic AI)\n* Adam Treat (Nomic AI)" + }, + { + "version": "3.5.2", + "notes": "* **Model Search:** There are now separate tabs for official and third-party models.\n* **Local Server Fixes:** Several mistakes in v3.5's changes to the API server have been corrected.\n* **Cloned Model Fixes:** The chat template and system message of cloned models now manage their defaults correctly.\n* **Translation Improvements:** The Romanian and Italian translations have been updated.\n", + "contributors": "* Jared Van Bortel (Nomic AI)\n* Adam Treat (Nomic AI)\n* Riccardo Giovanetti (`@Harvester62`)\n* Victor Emanuel (`@SINAPSA-IC`)" + }, + { + "version": "3.5.3", + "notes": "* **LocalDocs Fix:** A serious issue causing LocalDocs to not work properly in v3.5.2 has been fixed.\n", + "contributors": "* Jared Van Bortel (Nomic AI)\n* Adam Treat (Nomic AI)" + }, + { + "version": "3.6.0", + "notes": "* **Reasoner v1:**\n * Built-in javascript code interpreter tool.\n * Custom curated model that utilizes the code interpreter to break down, analyze, perform, and verify complex reasoning tasks.\n* **Templates:** Automatically substitute chat templates that are not compatible with Jinja2Cpp in GGUFs.\n* **Fixes:**\n * Remote model template to allow for XML in messages.\n * Jinja2Cpp bug that broke system message detection in chat templates.\n * LocalDocs sources displaying in unconsolidated form after v3.5.0.\n", + "contributors": "* Adam Treat (Nomic AI)\n* Jared Van Bortel (Nomic AI)" + }, + { + "version": "3.6.1", + "notes": "* **Fixes:**\n * The stop generation button no longer working in v3.6.0.\n * The copy entire conversation button no longer working in v3.6.0.\n", + "contributors": "* Adam Treat (Nomic AI)" + }, + { + "version": "3.7.0", + "notes": "* **Windows ARM Support:** GPT4All now supports the Windows ARM platform, ensuring compatibility with devices powered by Qualcomm Snapdragon and Microsoft SQ-series processors.\n * **NOTE:** Support for GPU and/or NPU acceleration is not available at this time. Only the CPU will be used to run LLMs.\n * **NOTE:** You must install the new *Windows ARM* version of GPT4All from the website. The standard *Windows* version will not work due to emulation limitations.\n* **Fixed Updating on macOS:** The maintenance tool no longer crashes when attempting to update or uninstall GPT4All on Sequoia.\n * **NOTE:** If you have installed the version from the GitHub releases as a workaround for this issue, you can safely uninstall it and switch back to the version from the website.\n* **Fixed Chat Saving on macOS:** Chats now save as expected when the application is quit with Command-Q.\n* **Code Interpreter Improvements:**\n * The behavior when the code takes too long to execute and times out has been improved.\n * console.log now accepts multiple arguments for better compatibility with native JavaScript.\n* **Chat Templating Improvements:**\n * Two crashes and one compatibility issue have been fixed in the chat template parser.\n * The default chat template for EM German Mistral has been fixed.\n * Automatic replacements have been added for five new models as we continue to improve compatibility with common chat templates.\n", + "contributors": "* Jared Van Bortel (Nomic AI)\n* Adam Treat (Nomic AI)\n* Riccardo Giovanetti (`@Harvester62`)" + }, + { + "version": "3.8.0", + "notes": "* **Native DeepSeek-R1-Distill Support:** GPT4All now has robust support for the DeepSeek-R1 family of distillations.\n * Several model variants are now available on the downloads page.\n * Reasoning (wrapped in \"think\" tags) is displayed similarly to the Reasoner model.\n * The DeepSeek-R1 Qwen pretokenizer is now supported, resolving the loading failure in previous versions.\n * The model is now configured with a GPT4All-compatible prompt template by default.\n* **Chat Templating Overhaul:** The template parser has been *completely* replaced with one that has much better compatibility with common models.\n* **Code Interpreter Fixes:**\n * An issue preventing the code interpreter from logging a single string in v3.7.0 has been fixed.\n * The UI no longer freezes while the code interpreter is running a computation.\n* **Local Server Fixes:**\n * An issue preventing the server from using LocalDocs after the first request since v3.5.0 has been fixed.\n * System messages are now correctly hidden from the message history.\n", + "contributors": "* Jared Van Bortel (Nomic AI)\n* Adam Treat (Nomic AI)\n* ThiloteE (`@ThiloteE`)" } ] diff --git a/gpt4all-chat/qml/AddGPT4AllModelView.qml b/gpt4all-chat/qml/AddGPT4AllModelView.qml new file mode 100644 index 000000000000..2a1832af14fa --- /dev/null +++ b/gpt4all-chat/qml/AddGPT4AllModelView.qml @@ -0,0 +1,592 @@ +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick.Dialogs +import Qt.labs.folderlistmodel +import Qt5Compat.GraphicalEffects + +import llm +import chatlistmodel +import download +import modellist +import network +import gpt4all +import mysettings +import localdocs + +ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: 5 + + Label { + Layout.topMargin: 0 + Layout.bottomMargin: 25 + Layout.rightMargin: 150 * theme.fontScale + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + verticalAlignment: Text.AlignTop + text: qsTr("These models have been specifically configured for use in GPT4All. The first few models on the " + + "list are known to work the best, but you should only attempt to use models that will fit in your " + + "available memory.") + font.pixelSize: theme.fontSizeLarger + color: theme.textColor + wrapMode: Text.WordWrap + } + + Label { + visible: !ModelList.gpt4AllDownloadableModels.count && !ModelList.asyncModelRequestOngoing + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: Qt.AlignHCenter + verticalAlignment: Qt.AlignVCenter + text: qsTr("Network error: could not retrieve %1").arg("http://gpt4all.io/models/models3.json") + font.pixelSize: theme.fontSizeLarge + color: theme.mutedTextColor + } + + MyBusyIndicator { + visible: !ModelList.gpt4AllDownloadableModels.count && ModelList.asyncModelRequestOngoing + running: ModelList.asyncModelRequestOngoing + Accessible.role: Accessible.Animation + Layout.alignment: Qt.AlignCenter + Accessible.name: qsTr("Busy indicator") + Accessible.description: qsTr("Displayed when the models request is ongoing") + } + + RowLayout { + ButtonGroup { + id: buttonGroup + exclusive: true + } + MyButton { + text: qsTr("All") + checked: true + borderWidth: 0 + backgroundColor: checked ? theme.lightButtonBackground : "transparent" + backgroundColorHovered: theme.lighterButtonBackgroundHovered + backgroundRadius: 5 + padding: 15 + topPadding: 8 + bottomPadding: 8 + textColor: theme.lighterButtonForeground + fontPixelSize: theme.fontSizeLarge + fontPixelBold: true + checkable: true + ButtonGroup.group: buttonGroup + onClicked: { + ModelList.gpt4AllDownloadableModels.filter(""); + } + + } + MyButton { + text: qsTr("Reasoning") + borderWidth: 0 + backgroundColor: checked ? theme.lightButtonBackground : "transparent" + backgroundColorHovered: theme.lighterButtonBackgroundHovered + backgroundRadius: 5 + padding: 15 + topPadding: 8 + bottomPadding: 8 + textColor: theme.lighterButtonForeground + fontPixelSize: theme.fontSizeLarge + fontPixelBold: true + checkable: true + ButtonGroup.group: buttonGroup + onClicked: { + ModelList.gpt4AllDownloadableModels.filter("#reasoning"); + } + } + Layout.bottomMargin: 10 + } + + ScrollView { + id: scrollView + ScrollBar.vertical.policy: ScrollBar.AsNeeded + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + ListView { + id: modelListView + model: ModelList.gpt4AllDownloadableModels + boundsBehavior: Flickable.StopAtBounds + spacing: 30 + + delegate: Rectangle { + id: delegateItem + width: modelListView.width + height: childrenRect.height + 60 + color: theme.conversationBackground + radius: 10 + border.width: 1 + border.color: theme.controlBorder + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 30 + + Text { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + text: name + elide: Text.ElideRight + color: theme.titleTextColor + font.pixelSize: theme.fontSizeLargest + font.bold: true + Accessible.role: Accessible.Paragraph + Accessible.name: qsTr("Model file") + Accessible.description: qsTr("Model file to be downloaded") + } + + + Rectangle { + Layout.fillWidth: true + height: 1 + color: theme.dividerColor + } + + RowLayout { + Layout.topMargin: 10 + Layout.fillWidth: true + Text { + id: descriptionText + text: description + font.pixelSize: theme.fontSizeLarge + Layout.fillWidth: true + wrapMode: Text.WordWrap + textFormat: Text.StyledText + color: theme.textColor + linkColor: theme.textColor + Accessible.role: Accessible.Paragraph + Accessible.name: qsTr("Description") + Accessible.description: qsTr("File description") + onLinkActivated: function(link) { Qt.openUrlExternally(link); } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton // pass clicks to parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + + // FIXME Need to overhaul design here which must take into account + // features not present in current figma including: + // * Ability to cancel a current download + // * Ability to resume a download + // * The presentation of an error if encountered + // * Whether to show already installed models + // * Install of remote models with API keys + // * The presentation of the progress bar + Rectangle { + id: actionBox + width: childrenRect.width + 20 + color: "transparent" + border.width: 1 + border.color: theme.dividerColor + radius: 10 + Layout.rightMargin: 20 + Layout.bottomMargin: 20 + Layout.minimumHeight: childrenRect.height + 20 + Layout.alignment: Qt.AlignRight | Qt.AlignTop + + ColumnLayout { + spacing: 0 + MySettingsButton { + id: downloadButton + text: isDownloading ? qsTr("Cancel") : isIncomplete ? qsTr("Resume") : qsTr("Download") + font.pixelSize: theme.fontSizeLarge + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + visible: !isOnline && !installed && !calcHash && downloadError === "" + Accessible.description: qsTr("Stop/restart/start the download") + onClicked: { + if (!isDownloading) { + Download.downloadModel(filename); + } else { + Download.cancelDownload(filename); + } + } + } + + MySettingsDestructiveButton { + id: removeButton + text: qsTr("Remove") + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + visible: !isDownloading && (installed || isIncomplete) + Accessible.description: qsTr("Remove model from filesystem") + onClicked: { + Download.removeModel(filename); + } + } + + MySettingsButton { + id: installButton + visible: !installed && isOnline + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + text: qsTr("Install") + font.pixelSize: theme.fontSizeLarge + onClicked: { + var apiKeyText = apiKey.text.trim(), + baseUrlText = baseUrl.text.trim(), + modelNameText = modelName.text.trim(); + + var apiKeyOk = apiKeyText !== "", + baseUrlOk = !isCompatibleApi || baseUrlText !== "", + modelNameOk = !isCompatibleApi || modelNameText !== ""; + + if (!apiKeyOk) + apiKey.showError(); + if (!baseUrlOk) + baseUrl.showError(); + if (!modelNameOk) + modelName.showError(); + + if (!apiKeyOk || !baseUrlOk || !modelNameOk) + return; + + if (!isCompatibleApi) + Download.installModel( + filename, + apiKeyText, + ); + else + Download.installCompatibleModel( + modelNameText, + apiKeyText, + baseUrlText, + ); + } + Accessible.role: Accessible.Button + Accessible.name: qsTr("Install") + Accessible.description: qsTr("Install online model") + } + + ColumnLayout { + spacing: 0 + Label { + Layout.topMargin: 20 + Layout.leftMargin: 20 + visible: downloadError !== "" + textFormat: Text.StyledText + text: qsTr("Error") + color: theme.textColor + font.pixelSize: theme.fontSizeLarge + linkColor: theme.textErrorColor + Accessible.role: Accessible.Paragraph + Accessible.name: text + Accessible.description: qsTr("Describes an error that occurred when downloading") + onLinkActivated: { + downloadingErrorPopup.text = downloadError; + downloadingErrorPopup.open(); + } + } + + Label { + visible: LLM.systemTotalRAMInGB() < ramrequired + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.maximumWidth: 300 + textFormat: Text.StyledText + text: qsTr("WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).").arg(ramrequired).arg(LLM.systemTotalRAMInGBString()) + color: theme.textErrorColor + font.pixelSize: theme.fontSizeLarge + wrapMode: Text.WordWrap + Accessible.role: Accessible.Paragraph + Accessible.name: text + Accessible.description: qsTr("Error for incompatible hardware") + onLinkActivated: { + downloadingErrorPopup.text = downloadError; + downloadingErrorPopup.open(); + } + } + } + + ColumnLayout { + visible: isDownloading && !calcHash + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + spacing: 20 + + ProgressBar { + id: itemProgressBar + Layout.fillWidth: true + width: 200 + value: bytesReceived / bytesTotal + background: Rectangle { + implicitHeight: 45 + color: theme.progressBackground + radius: 3 + } + contentItem: Item { + implicitHeight: 40 + + Rectangle { + width: itemProgressBar.visualPosition * parent.width + height: parent.height + radius: 2 + color: theme.progressForeground + } + } + Accessible.role: Accessible.ProgressBar + Accessible.name: qsTr("Download progressBar") + Accessible.description: qsTr("Shows the progress made in the download") + } + + Label { + id: speedLabel + color: theme.textColor + Layout.alignment: Qt.AlignRight + text: speed + font.pixelSize: theme.fontSizeLarge + Accessible.role: Accessible.Paragraph + Accessible.name: qsTr("Download speed") + Accessible.description: qsTr("Download speed in bytes/kilobytes/megabytes per second") + } + } + + RowLayout { + visible: calcHash + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.maximumWidth: 200 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + clip: true + + Label { + id: calcHashLabel + color: theme.textColor + text: qsTr("Calculating...") + font.pixelSize: theme.fontSizeLarge + Accessible.role: Accessible.Paragraph + Accessible.name: text + Accessible.description: qsTr("Whether the file hash is being calculated") + } + + MyBusyIndicator { + id: busyCalcHash + running: calcHash + Accessible.role: Accessible.Animation + Accessible.name: qsTr("Busy indicator") + Accessible.description: qsTr("Displayed when the file hash is being calculated") + } + } + + MyTextField { + id: apiKey + visible: !installed && isOnline + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + wrapMode: Text.WrapAnywhere + function showError() { + messageToast.show(qsTr("ERROR: $API_KEY is empty.")); + apiKey.placeholderTextColor = theme.textErrorColor; + } + onTextChanged: { + apiKey.placeholderTextColor = theme.mutedTextColor; + } + placeholderText: qsTr("enter $API_KEY") + Accessible.role: Accessible.EditableText + Accessible.name: placeholderText + Accessible.description: qsTr("Whether the file hash is being calculated") + } + + MyTextField { + id: baseUrl + visible: !installed && isOnline && isCompatibleApi + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + wrapMode: Text.WrapAnywhere + function showError() { + messageToast.show(qsTr("ERROR: $BASE_URL is empty.")); + baseUrl.placeholderTextColor = theme.textErrorColor; + } + onTextChanged: { + baseUrl.placeholderTextColor = theme.mutedTextColor; + } + placeholderText: qsTr("enter $BASE_URL") + Accessible.role: Accessible.EditableText + Accessible.name: placeholderText + Accessible.description: qsTr("Whether the file hash is being calculated") + } + + MyTextField { + id: modelName + visible: !installed && isOnline && isCompatibleApi + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + wrapMode: Text.WrapAnywhere + function showError() { + messageToast.show(qsTr("ERROR: $MODEL_NAME is empty.")) + modelName.placeholderTextColor = theme.textErrorColor; + } + onTextChanged: { + modelName.placeholderTextColor = theme.mutedTextColor; + } + placeholderText: qsTr("enter $MODEL_NAME") + Accessible.role: Accessible.EditableText + Accessible.name: placeholderText + Accessible.description: qsTr("Whether the file hash is being calculated") + } + } + } + } + + Item { + Layout.minimumWidth: childrenRect.width + Layout.minimumHeight: childrenRect.height + Layout.bottomMargin: 10 + RowLayout { + id: paramRow + anchors.centerIn: parent + ColumnLayout { + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Text { + text: qsTr("File size") + font.pixelSize: theme.fontSizeSmall + color: theme.mutedDarkTextColor + } + Text { + text: filesize + color: theme.textColor + font.pixelSize: theme.fontSizeSmall + font.bold: true + } + } + Rectangle { + width: 1 + Layout.fillHeight: true + color: theme.dividerColor + } + ColumnLayout { + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Text { + text: qsTr("RAM required") + font.pixelSize: theme.fontSizeSmall + color: theme.mutedDarkTextColor + } + Text { + text: ramrequired >= 0 ? qsTr("%1 GB").arg(ramrequired) : qsTr("?") + color: theme.textColor + font.pixelSize: theme.fontSizeSmall + font.bold: true + } + } + Rectangle { + width: 1 + Layout.fillHeight: true + color: theme.dividerColor + } + ColumnLayout { + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Text { + text: qsTr("Parameters") + font.pixelSize: theme.fontSizeSmall + color: theme.mutedDarkTextColor + } + Text { + text: parameters !== "" ? parameters : qsTr("?") + color: theme.textColor + font.pixelSize: theme.fontSizeSmall + font.bold: true + } + } + Rectangle { + width: 1 + Layout.fillHeight: true + color: theme.dividerColor + } + ColumnLayout { + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Text { + text: qsTr("Quant") + font.pixelSize: theme.fontSizeSmall + color: theme.mutedDarkTextColor + } + Text { + text: quant + color: theme.textColor + font.pixelSize: theme.fontSizeSmall + font.bold: true + } + } + Rectangle { + width: 1 + Layout.fillHeight: true + color: theme.dividerColor + } + ColumnLayout { + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Text { + text: qsTr("Type") + font.pixelSize: theme.fontSizeSmall + color: theme.mutedDarkTextColor + } + Text { + text: type + color: theme.textColor + font.pixelSize: theme.fontSizeSmall + font.bold: true + } + } + } + + Rectangle { + color: "transparent" + anchors.fill: paramRow + border.color: theme.dividerColor + border.width: 1 + radius: 10 + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: theme.dividerColor + } + } + } + } + } +} diff --git a/gpt4all-chat/qml/AddHFModelView.qml b/gpt4all-chat/qml/AddHFModelView.qml new file mode 100644 index 000000000000..0435e2b5f8c8 --- /dev/null +++ b/gpt4all-chat/qml/AddHFModelView.qml @@ -0,0 +1,703 @@ +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import QtQuick.Layouts +import QtQuick.Dialogs +import Qt.labs.folderlistmodel +import Qt5Compat.GraphicalEffects + +import llm +import chatlistmodel +import download +import modellist +import network +import gpt4all +import mysettings +import localdocs + +ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignTop + spacing: 5 + + Label { + Layout.topMargin: 0 + Layout.bottomMargin: 25 + Layout.rightMargin: 150 * theme.fontScale + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + verticalAlignment: Text.AlignTop + text: qsTr("Use the search to find and download models from HuggingFace. There is NO GUARANTEE that these " + + "will work. Many will require additional configuration before they can be used.") + font.pixelSize: theme.fontSizeLarger + color: theme.textColor + wrapMode: Text.WordWrap + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignCenter + Layout.margins: 0 + spacing: 10 + MyTextField { + id: discoverField + property string textBeingSearched: "" + readOnly: ModelList.discoverInProgress + Layout.alignment: Qt.AlignCenter + Layout.fillWidth: true + font.pixelSize: theme.fontSizeLarger + placeholderText: qsTr("Discover and download models by keyword search...") + Accessible.role: Accessible.EditableText + Accessible.name: placeholderText + Accessible.description: qsTr("Text field for discovering and filtering downloadable models") + Connections { + target: ModelList + function onDiscoverInProgressChanged() { + if (ModelList.discoverInProgress) { + discoverField.textBeingSearched = discoverField.text; + discoverField.text = qsTr("Searching \u00B7 %1").arg(discoverField.textBeingSearched); + } else { + discoverField.text = discoverField.textBeingSearched; + discoverField.textBeingSearched = ""; + } + } + } + background: ProgressBar { + id: discoverProgressBar + indeterminate: ModelList.discoverInProgress && ModelList.discoverProgress === 0.0 + value: ModelList.discoverProgress + background: Rectangle { + color: theme.controlBackground + border.color: theme.controlBorder + radius: 10 + } + contentItem: Item { + Rectangle { + visible: ModelList.discoverInProgress + anchors.bottom: parent.bottom + width: discoverProgressBar.visualPosition * parent.width + height: 10 + radius: 2 + color: theme.progressForeground + } + } + } + + Keys.onReturnPressed: (event)=> { + if (event.modifiers & Qt.ControlModifier || event.modifiers & Qt.ShiftModifier) + event.accepted = false; + else { + editingFinished(); + sendDiscovery() + } + } + function sendDiscovery() { + ModelList.huggingFaceDownloadableModels.discoverAndFilter(discoverField.text); + } + RowLayout { + spacing: 0 + anchors.right: discoverField.right + anchors.verticalCenter: discoverField.verticalCenter + anchors.rightMargin: 15 + visible: !ModelList.discoverInProgress + MyMiniButton { + id: clearDiscoverButton + backgroundColor: theme.textColor + backgroundColorHovered: theme.iconBackgroundDark + visible: discoverField.text !== "" + source: "qrc:/gpt4all/icons/close.svg" + onClicked: { + discoverField.text = "" + discoverField.sendDiscovery() // should clear results + } + } + MyMiniButton { + backgroundColor: theme.textColor + backgroundColorHovered: theme.iconBackgroundDark + source: "qrc:/gpt4all/icons/settings.svg" + onClicked: { + discoveryTools.visible = !discoveryTools.visible + } + } + MyMiniButton { + id: sendButton + enabled: !ModelList.discoverInProgress + backgroundColor: theme.textColor + backgroundColorHovered: theme.iconBackgroundDark + source: "qrc:/gpt4all/icons/send_message.svg" + Accessible.name: qsTr("Initiate model discovery and filtering") + Accessible.description: qsTr("Triggers discovery and filtering of models") + onClicked: { + discoverField.sendDiscovery() + } + } + } + } + } + + RowLayout { + id: discoveryTools + Layout.fillWidth: true + Layout.alignment: Qt.AlignCenter + Layout.margins: 0 + spacing: 20 + visible: false + MyComboBox { + id: comboSort + model: ListModel { + ListElement { name: qsTr("Default") } + ListElement { name: qsTr("Likes") } + ListElement { name: qsTr("Downloads") } + ListElement { name: qsTr("Recent") } + } + currentIndex: ModelList.discoverSort + contentItem: Text { + anchors.horizontalCenter: parent.horizontalCenter + rightPadding: 30 + color: theme.textColor + text: { + return qsTr("Sort by: %1").arg(comboSort.displayText) + } + font.pixelSize: theme.fontSizeLarger + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + onActivated: function (index) { + ModelList.discoverSort = index; + } + } + MyComboBox { + id: comboSortDirection + model: ListModel { + ListElement { name: qsTr("Asc") } + ListElement { name: qsTr("Desc") } + } + currentIndex: { + if (ModelList.discoverSortDirection === 1) + return 0 + else + return 1; + } + contentItem: Text { + anchors.horizontalCenter: parent.horizontalCenter + rightPadding: 30 + color: theme.textColor + text: { + return qsTr("Sort dir: %1").arg(comboSortDirection.displayText) + } + font.pixelSize: theme.fontSizeLarger + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + onActivated: function (index) { + if (index === 0) + ModelList.discoverSortDirection = 1; + else + ModelList.discoverSortDirection = -1; + } + } + MyComboBox { + id: comboLimit + model: ListModel { + ListElement { name: "5" } + ListElement { name: "10" } + ListElement { name: "20" } + ListElement { name: "50" } + ListElement { name: "100" } + ListElement { name: qsTr("None") } + } + + currentIndex: { + if (ModelList.discoverLimit === 5) + return 0; + else if (ModelList.discoverLimit === 10) + return 1; + else if (ModelList.discoverLimit === 20) + return 2; + else if (ModelList.discoverLimit === 50) + return 3; + else if (ModelList.discoverLimit === 100) + return 4; + else if (ModelList.discoverLimit === -1) + return 5; + } + contentItem: Text { + anchors.horizontalCenter: parent.horizontalCenter + rightPadding: 30 + color: theme.textColor + text: { + return qsTr("Limit: %1").arg(comboLimit.displayText) + } + font.pixelSize: theme.fontSizeLarger + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + } + onActivated: function (index) { + switch (index) { + case 0: + ModelList.discoverLimit = 5; break; + case 1: + ModelList.discoverLimit = 10; break; + case 2: + ModelList.discoverLimit = 20; break; + case 3: + ModelList.discoverLimit = 50; break; + case 4: + ModelList.discoverLimit = 100; break; + case 5: + ModelList.discoverLimit = -1; break; + } + } + } + } + + ScrollView { + id: scrollView + ScrollBar.vertical.policy: ScrollBar.AsNeeded + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + + ListView { + id: modelListView + model: ModelList.huggingFaceDownloadableModels + boundsBehavior: Flickable.StopAtBounds + spacing: 30 + + delegate: Rectangle { + id: delegateItem + width: modelListView.width + height: childrenRect.height + 60 + color: theme.conversationBackground + radius: 10 + border.width: 1 + border.color: theme.controlBorder + + ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 30 + + Text { + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + text: name + elide: Text.ElideRight + color: theme.titleTextColor + font.pixelSize: theme.fontSizeLargest + font.bold: true + Accessible.role: Accessible.Paragraph + Accessible.name: qsTr("Model file") + Accessible.description: qsTr("Model file to be downloaded") + } + + + Rectangle { + Layout.fillWidth: true + height: 1 + color: theme.dividerColor + } + + RowLayout { + Layout.topMargin: 10 + Layout.fillWidth: true + Text { + id: descriptionText + text: description + font.pixelSize: theme.fontSizeLarge + Layout.fillWidth: true + wrapMode: Text.WordWrap + textFormat: Text.StyledText + color: theme.textColor + linkColor: theme.textColor + Accessible.role: Accessible.Paragraph + Accessible.name: qsTr("Description") + Accessible.description: qsTr("File description") + onLinkActivated: function(link) { Qt.openUrlExternally(link); } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton // pass clicks to parent + cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor + } + } + + // FIXME Need to overhaul design here which must take into account + // features not present in current figma including: + // * Ability to cancel a current download + // * Ability to resume a download + // * The presentation of an error if encountered + // * Whether to show already installed models + // * Install of remote models with API keys + // * The presentation of the progress bar + Rectangle { + id: actionBox + width: childrenRect.width + 20 + color: "transparent" + border.width: 1 + border.color: theme.dividerColor + radius: 10 + Layout.rightMargin: 20 + Layout.bottomMargin: 20 + Layout.minimumHeight: childrenRect.height + 20 + Layout.alignment: Qt.AlignRight | Qt.AlignTop + + ColumnLayout { + spacing: 0 + MySettingsButton { + id: downloadButton + text: isDownloading ? qsTr("Cancel") : isIncomplete ? qsTr("Resume") : qsTr("Download") + font.pixelSize: theme.fontSizeLarge + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + visible: !isOnline && !installed && !calcHash && downloadError === "" + Accessible.description: qsTr("Stop/restart/start the download") + onClicked: { + if (!isDownloading) { + Download.downloadModel(filename); + } else { + Download.cancelDownload(filename); + } + } + } + + MySettingsDestructiveButton { + id: removeButton + text: qsTr("Remove") + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + visible: !isDownloading && (installed || isIncomplete) + Accessible.description: qsTr("Remove model from filesystem") + onClicked: { + Download.removeModel(filename); + } + } + + MySettingsButton { + id: installButton + visible: !installed && isOnline + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + text: qsTr("Install") + font.pixelSize: theme.fontSizeLarge + onClicked: { + var apiKeyText = apiKey.text.trim(), + baseUrlText = baseUrl.text.trim(), + modelNameText = modelName.text.trim(); + + var apiKeyOk = apiKeyText !== "", + baseUrlOk = !isCompatibleApi || baseUrlText !== "", + modelNameOk = !isCompatibleApi || modelNameText !== ""; + + if (!apiKeyOk) + apiKey.showError(); + if (!baseUrlOk) + baseUrl.showError(); + if (!modelNameOk) + modelName.showError(); + + if (!apiKeyOk || !baseUrlOk || !modelNameOk) + return; + + if (!isCompatibleApi) + Download.installModel( + filename, + apiKeyText, + ); + else + Download.installCompatibleModel( + modelNameText, + apiKeyText, + baseUrlText, + ); + } + Accessible.role: Accessible.Button + Accessible.name: qsTr("Install") + Accessible.description: qsTr("Install online model") + } + + ColumnLayout { + spacing: 0 + Label { + Layout.topMargin: 20 + Layout.leftMargin: 20 + visible: downloadError !== "" + textFormat: Text.StyledText + text: qsTr("Error") + color: theme.textColor + font.pixelSize: theme.fontSizeLarge + linkColor: theme.textErrorColor + Accessible.role: Accessible.Paragraph + Accessible.name: text + Accessible.description: qsTr("Describes an error that occurred when downloading") + onLinkActivated: { + downloadingErrorPopup.text = downloadError; + downloadingErrorPopup.open(); + } + } + + Label { + visible: LLM.systemTotalRAMInGB() < ramrequired + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.maximumWidth: 300 + textFormat: Text.StyledText + text: qsTr("WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).").arg(ramrequired).arg(LLM.systemTotalRAMInGBString()) + color: theme.textErrorColor + font.pixelSize: theme.fontSizeLarge + wrapMode: Text.WordWrap + Accessible.role: Accessible.Paragraph + Accessible.name: text + Accessible.description: qsTr("Error for incompatible hardware") + onLinkActivated: { + downloadingErrorPopup.text = downloadError; + downloadingErrorPopup.open(); + } + } + } + + ColumnLayout { + visible: isDownloading && !calcHash + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + spacing: 20 + + ProgressBar { + id: itemProgressBar + Layout.fillWidth: true + width: 200 + value: bytesReceived / bytesTotal + background: Rectangle { + implicitHeight: 45 + color: theme.progressBackground + radius: 3 + } + contentItem: Item { + implicitHeight: 40 + + Rectangle { + width: itemProgressBar.visualPosition * parent.width + height: parent.height + radius: 2 + color: theme.progressForeground + } + } + Accessible.role: Accessible.ProgressBar + Accessible.name: qsTr("Download progressBar") + Accessible.description: qsTr("Shows the progress made in the download") + } + + Label { + id: speedLabel + color: theme.textColor + Layout.alignment: Qt.AlignRight + text: speed + font.pixelSize: theme.fontSizeLarge + Accessible.role: Accessible.Paragraph + Accessible.name: qsTr("Download speed") + Accessible.description: qsTr("Download speed in bytes/kilobytes/megabytes per second") + } + } + + RowLayout { + visible: calcHash + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.maximumWidth: 200 + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + clip: true + + Label { + id: calcHashLabel + color: theme.textColor + text: qsTr("Calculating...") + font.pixelSize: theme.fontSizeLarge + Accessible.role: Accessible.Paragraph + Accessible.name: text + Accessible.description: qsTr("Whether the file hash is being calculated") + } + + MyBusyIndicator { + id: busyCalcHash + running: calcHash + Accessible.role: Accessible.Animation + Accessible.name: qsTr("Busy indicator") + Accessible.description: qsTr("Displayed when the file hash is being calculated") + } + } + + MyTextField { + id: apiKey + visible: !installed && isOnline + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + wrapMode: Text.WrapAnywhere + function showError() { + messageToast.show(qsTr("ERROR: $API_KEY is empty.")); + apiKey.placeholderTextColor = theme.textErrorColor; + } + onTextChanged: { + apiKey.placeholderTextColor = theme.mutedTextColor; + } + placeholderText: qsTr("enter $API_KEY") + Accessible.role: Accessible.EditableText + Accessible.name: placeholderText + Accessible.description: qsTr("Whether the file hash is being calculated") + } + + MyTextField { + id: baseUrl + visible: !installed && isOnline && isCompatibleApi + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + wrapMode: Text.WrapAnywhere + function showError() { + messageToast.show(qsTr("ERROR: $BASE_URL is empty.")); + baseUrl.placeholderTextColor = theme.textErrorColor; + } + onTextChanged: { + baseUrl.placeholderTextColor = theme.mutedTextColor; + } + placeholderText: qsTr("enter $BASE_URL") + Accessible.role: Accessible.EditableText + Accessible.name: placeholderText + Accessible.description: qsTr("Whether the file hash is being calculated") + } + + MyTextField { + id: modelName + visible: !installed && isOnline && isCompatibleApi + Layout.topMargin: 20 + Layout.leftMargin: 20 + Layout.minimumWidth: 200 + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + wrapMode: Text.WrapAnywhere + function showError() { + messageToast.show(qsTr("ERROR: $MODEL_NAME is empty.")) + modelName.placeholderTextColor = theme.textErrorColor; + } + onTextChanged: { + modelName.placeholderTextColor = theme.mutedTextColor; + } + placeholderText: qsTr("enter $MODEL_NAME") + Accessible.role: Accessible.EditableText + Accessible.name: placeholderText + Accessible.description: qsTr("Whether the file hash is being calculated") + } + } + } + } + + Item { + Layout.minimumWidth: childrenRect.width + Layout.minimumHeight: childrenRect.height + Layout.bottomMargin: 10 + RowLayout { + id: paramRow + anchors.centerIn: parent + ColumnLayout { + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Text { + text: qsTr("File size") + font.pixelSize: theme.fontSizeSmall + color: theme.mutedDarkTextColor + } + Text { + text: filesize + color: theme.textColor + font.pixelSize: theme.fontSizeSmall + font.bold: true + } + } + Rectangle { + width: 1 + Layout.fillHeight: true + color: theme.dividerColor + } + ColumnLayout { + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Text { + text: qsTr("Quant") + font.pixelSize: theme.fontSizeSmall + color: theme.mutedDarkTextColor + } + Text { + text: quant + color: theme.textColor + font.pixelSize: theme.fontSizeSmall + font.bold: true + } + } + Rectangle { + width: 1 + Layout.fillHeight: true + color: theme.dividerColor + } + ColumnLayout { + Layout.topMargin: 10 + Layout.bottomMargin: 10 + Layout.leftMargin: 20 + Layout.rightMargin: 20 + Text { + text: qsTr("Type") + font.pixelSize: theme.fontSizeSmall + color: theme.mutedDarkTextColor + } + Text { + text: type + color: theme.textColor + font.pixelSize: theme.fontSizeSmall + font.bold: true + } + } + } + + Rectangle { + color: "transparent" + anchors.fill: paramRow + border.color: theme.dividerColor + border.width: 1 + radius: 10 + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: theme.dividerColor + } + } + } + } + } +} diff --git a/gpt4all-chat/qml/AddModelView.qml b/gpt4all-chat/qml/AddModelView.qml index d366b8f9626c..223d703791a9 100644 --- a/gpt4all-chat/qml/AddModelView.qml +++ b/gpt4all-chat/qml/AddModelView.qml @@ -42,12 +42,12 @@ Rectangle { anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: 30 - spacing: 30 + spacing: 10 ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: 30 + spacing: 10 MyButton { id: backButton @@ -76,732 +76,60 @@ Rectangle { font.pixelSize: theme.fontSizeBanner color: theme.titleTextColor } + } - RowLayout { - Layout.fillWidth: true - Layout.alignment: Qt.AlignCenter - Layout.margins: 0 - spacing: 10 - MyTextField { - id: discoverField - property string textBeingSearched: "" - readOnly: ModelList.discoverInProgress - Layout.alignment: Qt.AlignCenter - Layout.fillWidth: true - font.pixelSize: theme.fontSizeLarger - placeholderText: qsTr("Discover and download models by keyword search...") - Accessible.role: Accessible.EditableText - Accessible.name: placeholderText - Accessible.description: qsTr("Text field for discovering and filtering downloadable models") - Connections { - target: ModelList - function onDiscoverInProgressChanged() { - if (ModelList.discoverInProgress) { - discoverField.textBeingSearched = discoverField.text; - discoverField.text = qsTr("Searching \u00B7 %1").arg(discoverField.textBeingSearched); - } else { - discoverField.text = discoverField.textBeingSearched; - discoverField.textBeingSearched = ""; - } - } - } - background: ProgressBar { - id: discoverProgressBar - indeterminate: ModelList.discoverInProgress && ModelList.discoverProgress === 0.0 - value: ModelList.discoverProgress - background: Rectangle { - color: theme.controlBackground - border.color: theme.controlBorder - radius: 10 - } - contentItem: Item { - Rectangle { - visible: ModelList.discoverInProgress - anchors.bottom: parent.bottom - width: discoverProgressBar.visualPosition * parent.width - height: 10 - radius: 2 - color: theme.progressForeground - } - } - } - - Keys.onReturnPressed: (event)=> { - if (event.modifiers & Qt.ControlModifier || event.modifiers & Qt.ShiftModifier) - event.accepted = false; - else { - editingFinished(); - sendDiscovery() - } - } - function sendDiscovery() { - ModelList.downloadableModels.discoverAndFilter(discoverField.text); - } - RowLayout { - spacing: 0 - anchors.right: discoverField.right - anchors.verticalCenter: discoverField.verticalCenter - anchors.rightMargin: 15 - visible: !ModelList.discoverInProgress - MyMiniButton { - id: clearDiscoverButton - backgroundColor: theme.textColor - backgroundColorHovered: theme.iconBackgroundDark - visible: discoverField.text !== "" - source: "qrc:/gpt4all/icons/close.svg" - onClicked: { - discoverField.text = "" - discoverField.sendDiscovery() // should clear results - } - } - MyMiniButton { - backgroundColor: theme.textColor - backgroundColorHovered: theme.iconBackgroundDark - source: "qrc:/gpt4all/icons/settings.svg" - onClicked: { - discoveryTools.visible = !discoveryTools.visible - } - } - MyMiniButton { - id: sendButton - enabled: !ModelList.discoverInProgress - backgroundColor: theme.textColor - backgroundColorHovered: theme.iconBackgroundDark - source: "qrc:/gpt4all/icons/send_message.svg" - Accessible.name: qsTr("Initiate model discovery and filtering") - Accessible.description: qsTr("Triggers discovery and filtering of models") - onClicked: { - discoverField.sendDiscovery() - } - } - } + RowLayout { + id: bar + implicitWidth: 600 + spacing: 10 + MyTabButton { + text: qsTr("GPT4All") + isSelected: gpt4AllModelView.isShown() + onPressed: { + gpt4AllModelView.show(); } } - - RowLayout { - id: discoveryTools - Layout.fillWidth: true - Layout.alignment: Qt.AlignCenter - Layout.margins: 0 - spacing: 20 - visible: false - MyComboBox { - id: comboSort - model: ListModel { - ListElement { name: qsTr("Default") } - ListElement { name: qsTr("Likes") } - ListElement { name: qsTr("Downloads") } - ListElement { name: qsTr("Recent") } - } - currentIndex: ModelList.discoverSort - contentItem: Text { - anchors.horizontalCenter: parent.horizontalCenter - rightPadding: 30 - color: theme.textColor - text: { - return qsTr("Sort by: %1").arg(comboSort.displayText) - } - font.pixelSize: theme.fontSizeLarger - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } - onActivated: function (index) { - ModelList.discoverSort = index; - } - } - MyComboBox { - id: comboSortDirection - model: ListModel { - ListElement { name: qsTr("Asc") } - ListElement { name: qsTr("Desc") } - } - currentIndex: { - if (ModelList.discoverSortDirection === 1) - return 0 - else - return 1; - } - contentItem: Text { - anchors.horizontalCenter: parent.horizontalCenter - rightPadding: 30 - color: theme.textColor - text: { - return qsTr("Sort dir: %1").arg(comboSortDirection.displayText) - } - font.pixelSize: theme.fontSizeLarger - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } - onActivated: function (index) { - if (index === 0) - ModelList.discoverSortDirection = 1; - else - ModelList.discoverSortDirection = -1; - } - } - MyComboBox { - id: comboLimit - model: ListModel { - ListElement { name: "5" } - ListElement { name: "10" } - ListElement { name: "20" } - ListElement { name: "50" } - ListElement { name: "100" } - ListElement { name: qsTr("None") } - } - - currentIndex: { - if (ModelList.discoverLimit === 5) - return 0; - else if (ModelList.discoverLimit === 10) - return 1; - else if (ModelList.discoverLimit === 20) - return 2; - else if (ModelList.discoverLimit === 50) - return 3; - else if (ModelList.discoverLimit === 100) - return 4; - else if (ModelList.discoverLimit === -1) - return 5; - } - contentItem: Text { - anchors.horizontalCenter: parent.horizontalCenter - rightPadding: 30 - color: theme.textColor - text: { - return qsTr("Limit: %1").arg(comboLimit.displayText) - } - font.pixelSize: theme.fontSizeLarger - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - } - onActivated: function (index) { - switch (index) { - case 0: - ModelList.discoverLimit = 5; break; - case 1: - ModelList.discoverLimit = 10; break; - case 2: - ModelList.discoverLimit = 20; break; - case 3: - ModelList.discoverLimit = 50; break; - case 4: - ModelList.discoverLimit = 100; break; - case 5: - ModelList.discoverLimit = -1; break; - } - } + MyTabButton { + text: qsTr("HuggingFace") + isSelected: huggingfaceModelView.isShown() + onPressed: { + huggingfaceModelView.show(); } } } - Label { - visible: !ModelList.downloadableModels.count && !ModelList.asyncModelRequestOngoing - Layout.fillWidth: true - Layout.fillHeight: true - horizontalAlignment: Qt.AlignHCenter - verticalAlignment: Qt.AlignVCenter - text: qsTr("Network error: could not retrieve %1").arg("http://gpt4all.io/models/models3.json") - font.pixelSize: theme.fontSizeLarge - color: theme.mutedTextColor - } - - MyBusyIndicator { - visible: !ModelList.downloadableModels.count && ModelList.asyncModelRequestOngoing - running: ModelList.asyncModelRequestOngoing - Accessible.role: Accessible.Animation - Layout.alignment: Qt.AlignCenter - Accessible.name: qsTr("Busy indicator") - Accessible.description: qsTr("Displayed when the models request is ongoing") - } - - ScrollView { - id: scrollView - ScrollBar.vertical.policy: ScrollBar.AsNeeded + StackLayout { + id: stackLayout Layout.fillWidth: true Layout.fillHeight: true - clip: true - - ListView { - id: modelListView - model: ModelList.downloadableModels - boundsBehavior: Flickable.StopAtBounds - spacing: 30 - - delegate: Rectangle { - id: delegateItem - width: modelListView.width - height: childrenRect.height + 60 - color: theme.conversationBackground - radius: 10 - border.width: 1 - border.color: theme.controlBorder - - ColumnLayout { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.margins: 30 - - Text { - Layout.fillWidth: true - Layout.alignment: Qt.AlignLeft - text: name - elide: Text.ElideRight - color: theme.titleTextColor - font.pixelSize: theme.fontSizeLargest - font.bold: true - Accessible.role: Accessible.Paragraph - Accessible.name: qsTr("Model file") - Accessible.description: qsTr("Model file to be downloaded") - } - - - Rectangle { - Layout.fillWidth: true - height: 1 - color: theme.dividerColor - } - - RowLayout { - Layout.topMargin: 10 - Layout.fillWidth: true - Text { - id: descriptionText - text: description - font.pixelSize: theme.fontSizeLarge - Layout.fillWidth: true - wrapMode: Text.WordWrap - textFormat: Text.StyledText - color: theme.textColor - linkColor: theme.textColor - Accessible.role: Accessible.Paragraph - Accessible.name: qsTr("Description") - Accessible.description: qsTr("File description") - onLinkActivated: function(link) { Qt.openUrlExternally(link); } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton // pass clicks to parent - cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor - } - } - - // FIXME Need to overhaul design here which must take into account - // features not present in current figma including: - // * Ability to cancel a current download - // * Ability to resume a download - // * The presentation of an error if encountered - // * Whether to show already installed models - // * Install of remote models with API keys - // * The presentation of the progress bar - Rectangle { - id: actionBox - width: childrenRect.width + 20 - color: "transparent" - border.width: 1 - border.color: theme.dividerColor - radius: 10 - Layout.rightMargin: 20 - Layout.bottomMargin: 20 - Layout.minimumHeight: childrenRect.height + 20 - Layout.alignment: Qt.AlignRight | Qt.AlignTop - - ColumnLayout { - spacing: 0 - MySettingsButton { - id: downloadButton - text: isDownloading ? qsTr("Cancel") : isIncomplete ? qsTr("Resume") : qsTr("Download") - font.pixelSize: theme.fontSizeLarge - Layout.topMargin: 20 - Layout.leftMargin: 20 - Layout.minimumWidth: 200 - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - visible: !isOnline && !installed && !calcHash && downloadError === "" - Accessible.description: qsTr("Stop/restart/start the download") - onClicked: { - if (!isDownloading) { - Download.downloadModel(filename); - } else { - Download.cancelDownload(filename); - } - } - } - - MySettingsDestructiveButton { - id: removeButton - text: qsTr("Remove") - Layout.topMargin: 20 - Layout.leftMargin: 20 - Layout.minimumWidth: 200 - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - visible: !isDownloading && (installed || isIncomplete) - Accessible.description: qsTr("Remove model from filesystem") - onClicked: { - Download.removeModel(filename); - } - } - MySettingsButton { - id: installButton - visible: !installed && isOnline - Layout.topMargin: 20 - Layout.leftMargin: 20 - Layout.minimumWidth: 200 - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - text: qsTr("Install") - font.pixelSize: theme.fontSizeLarge - onClicked: { - var apiKeyText = apiKey.text.trim(), - baseUrlText = baseUrl.text.trim(), - modelNameText = modelName.text.trim(); - - var apiKeyOk = apiKeyText !== "", - baseUrlOk = !isCompatibleApi || baseUrlText !== "", - modelNameOk = !isCompatibleApi || modelNameText !== ""; - - if (!apiKeyOk) - apiKey.showError(); - if (!baseUrlOk) - baseUrl.showError(); - if (!modelNameOk) - modelName.showError(); - - if (!apiKeyOk || !baseUrlOk || !modelNameOk) - return; - - if (!isCompatibleApi) - Download.installModel( - filename, - apiKeyText, - ); - else - Download.installCompatibleModel( - modelNameText, - apiKeyText, - baseUrlText, - ); - } - Accessible.role: Accessible.Button - Accessible.name: qsTr("Install") - Accessible.description: qsTr("Install online model") - } - - ColumnLayout { - spacing: 0 - Label { - Layout.topMargin: 20 - Layout.leftMargin: 20 - visible: downloadError !== "" - textFormat: Text.StyledText - text: qsTr("Error") - color: theme.textColor - font.pixelSize: theme.fontSizeLarge - linkColor: theme.textErrorColor - Accessible.role: Accessible.Paragraph - Accessible.name: text - Accessible.description: qsTr("Describes an error that occurred when downloading") - onLinkActivated: { - downloadingErrorPopup.text = downloadError; - downloadingErrorPopup.open(); - } - } - - Label { - visible: LLM.systemTotalRAMInGB() < ramrequired - Layout.topMargin: 20 - Layout.leftMargin: 20 - Layout.maximumWidth: 300 - textFormat: Text.StyledText - text: qsTr("WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).").arg(ramrequired).arg(LLM.systemTotalRAMInGBString()) - color: theme.textErrorColor - font.pixelSize: theme.fontSizeLarge - wrapMode: Text.WordWrap - Accessible.role: Accessible.Paragraph - Accessible.name: text - Accessible.description: qsTr("Error for incompatible hardware") - onLinkActivated: { - downloadingErrorPopup.text = downloadError; - downloadingErrorPopup.open(); - } - } - } - - ColumnLayout { - visible: isDownloading && !calcHash - Layout.topMargin: 20 - Layout.leftMargin: 20 - Layout.minimumWidth: 200 - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - spacing: 20 - - ProgressBar { - id: itemProgressBar - Layout.fillWidth: true - width: 200 - value: bytesReceived / bytesTotal - background: Rectangle { - implicitHeight: 45 - color: theme.progressBackground - radius: 3 - } - contentItem: Item { - implicitHeight: 40 - - Rectangle { - width: itemProgressBar.visualPosition * parent.width - height: parent.height - radius: 2 - color: theme.progressForeground - } - } - Accessible.role: Accessible.ProgressBar - Accessible.name: qsTr("Download progressBar") - Accessible.description: qsTr("Shows the progress made in the download") - } - - Label { - id: speedLabel - color: theme.textColor - Layout.alignment: Qt.AlignRight - text: speed - font.pixelSize: theme.fontSizeLarge - Accessible.role: Accessible.Paragraph - Accessible.name: qsTr("Download speed") - Accessible.description: qsTr("Download speed in bytes/kilobytes/megabytes per second") - } - } - - RowLayout { - visible: calcHash - Layout.topMargin: 20 - Layout.leftMargin: 20 - Layout.minimumWidth: 200 - Layout.maximumWidth: 200 - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - clip: true - - Label { - id: calcHashLabel - color: theme.textColor - text: qsTr("Calculating...") - font.pixelSize: theme.fontSizeLarge - Accessible.role: Accessible.Paragraph - Accessible.name: text - Accessible.description: qsTr("Whether the file hash is being calculated") - } - - MyBusyIndicator { - id: busyCalcHash - running: calcHash - Accessible.role: Accessible.Animation - Accessible.name: qsTr("Busy indicator") - Accessible.description: qsTr("Displayed when the file hash is being calculated") - } - } - - MyTextField { - id: apiKey - visible: !installed && isOnline - Layout.topMargin: 20 - Layout.leftMargin: 20 - Layout.minimumWidth: 200 - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - wrapMode: Text.WrapAnywhere - function showError() { - messageToast.show(qsTr("ERROR: $API_KEY is empty.")); - apiKey.placeholderTextColor = theme.textErrorColor; - } - onTextChanged: { - apiKey.placeholderTextColor = theme.mutedTextColor; - } - placeholderText: qsTr("enter $API_KEY") - Accessible.role: Accessible.EditableText - Accessible.name: placeholderText - Accessible.description: qsTr("Whether the file hash is being calculated") - } - - MyTextField { - id: baseUrl - visible: !installed && isOnline && isCompatibleApi - Layout.topMargin: 20 - Layout.leftMargin: 20 - Layout.minimumWidth: 200 - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - wrapMode: Text.WrapAnywhere - function showError() { - messageToast.show(qsTr("ERROR: $BASE_URL is empty.")); - baseUrl.placeholderTextColor = theme.textErrorColor; - } - onTextChanged: { - baseUrl.placeholderTextColor = theme.mutedTextColor; - } - placeholderText: qsTr("enter $BASE_URL") - Accessible.role: Accessible.EditableText - Accessible.name: placeholderText - Accessible.description: qsTr("Whether the file hash is being calculated") - } - - MyTextField { - id: modelName - visible: !installed && isOnline && isCompatibleApi - Layout.topMargin: 20 - Layout.leftMargin: 20 - Layout.minimumWidth: 200 - Layout.alignment: Qt.AlignTop | Qt.AlignHCenter - wrapMode: Text.WrapAnywhere - function showError() { - messageToast.show(qsTr("ERROR: $MODEL_NAME is empty.")) - modelName.placeholderTextColor = theme.textErrorColor; - } - onTextChanged: { - modelName.placeholderTextColor = theme.mutedTextColor; - } - placeholderText: qsTr("enter $MODEL_NAME") - Accessible.role: Accessible.EditableText - Accessible.name: placeholderText - Accessible.description: qsTr("Whether the file hash is being calculated") - } - } - } - } - - Item { - Layout.minimumWidth: childrenRect.width - Layout.minimumHeight: childrenRect.height - Layout.bottomMargin: 10 - RowLayout { - id: paramRow - anchors.centerIn: parent - ColumnLayout { - Layout.topMargin: 10 - Layout.bottomMargin: 10 - Layout.leftMargin: 20 - Layout.rightMargin: 20 - Text { - text: qsTr("File size") - font.pixelSize: theme.fontSizeSmall - color: theme.mutedDarkTextColor - } - Text { - text: filesize - color: theme.textColor - font.pixelSize: theme.fontSizeSmall - font.bold: true - } - } - Rectangle { - width: 1 - Layout.fillHeight: true - color: theme.dividerColor - } - ColumnLayout { - Layout.topMargin: 10 - Layout.bottomMargin: 10 - Layout.leftMargin: 20 - Layout.rightMargin: 20 - Text { - text: qsTr("RAM required") - font.pixelSize: theme.fontSizeSmall - color: theme.mutedDarkTextColor - } - Text { - text: ramrequired >= 0 ? qsTr("%1 GB").arg(ramrequired) : qsTr("?") - color: theme.textColor - font.pixelSize: theme.fontSizeSmall - font.bold: true - } - } - Rectangle { - width: 1 - Layout.fillHeight: true - color: theme.dividerColor - } - ColumnLayout { - Layout.topMargin: 10 - Layout.bottomMargin: 10 - Layout.leftMargin: 20 - Layout.rightMargin: 20 - Text { - text: qsTr("Parameters") - font.pixelSize: theme.fontSizeSmall - color: theme.mutedDarkTextColor - } - Text { - text: parameters !== "" ? parameters : qsTr("?") - color: theme.textColor - font.pixelSize: theme.fontSizeSmall - font.bold: true - } - } - Rectangle { - width: 1 - Layout.fillHeight: true - color: theme.dividerColor - } - ColumnLayout { - Layout.topMargin: 10 - Layout.bottomMargin: 10 - Layout.leftMargin: 20 - Layout.rightMargin: 20 - Text { - text: qsTr("Quant") - font.pixelSize: theme.fontSizeSmall - color: theme.mutedDarkTextColor - } - Text { - text: quant - color: theme.textColor - font.pixelSize: theme.fontSizeSmall - font.bold: true - } - } - Rectangle { - width: 1 - Layout.fillHeight: true - color: theme.dividerColor - } - ColumnLayout { - Layout.topMargin: 10 - Layout.bottomMargin: 10 - Layout.leftMargin: 20 - Layout.rightMargin: 20 - Text { - text: qsTr("Type") - font.pixelSize: theme.fontSizeSmall - color: theme.mutedDarkTextColor - } - Text { - text: type - color: theme.textColor - font.pixelSize: theme.fontSizeSmall - font.bold: true - } - } - } + AddGPT4AllModelView { + id: gpt4AllModelView + Layout.fillWidth: true + Layout.fillHeight: true - Rectangle { - color: "transparent" - anchors.fill: paramRow - border.color: theme.dividerColor - border.width: 1 - radius: 10 - } - } + function show() { + stackLayout.currentIndex = 0; + } + function isShown() { + return stackLayout.currentIndex === 0 + } + } - Rectangle { - Layout.fillWidth: true - height: 1 - color: theme.dividerColor - } - } + AddHFModelView { + id: huggingfaceModelView + Layout.fillWidth: true + Layout.fillHeight: true + // FIXME: This generates a warning and should not be used inside a layout, but without + // it the text field inside this qml does not display at full width so it looks like + // a bug in stacklayout + anchors.fill: parent + + function show() { + stackLayout.currentIndex = 1; + } + function isShown() { + return stackLayout.currentIndex === 1 } } } diff --git a/gpt4all-chat/qml/ApplicationSettings.qml b/gpt4all-chat/qml/ApplicationSettings.qml index e61fc274dad4..c002561563c9 100644 --- a/gpt4all-chat/qml/ApplicationSettings.qml +++ b/gpt4all-chat/qml/ApplicationSettings.qml @@ -10,7 +10,7 @@ import network import llm MySettingsTab { - onRestoreDefaultsClicked: { + onRestoreDefaults: { MySettings.restoreApplicationDefaults(); } title: qsTr("Application") @@ -486,23 +486,6 @@ MySettingsTab { Accessible.name: nThreadsLabel.text Accessible.description: ToolTip.text } - MySettingsLabel { - id: saveChatsContextLabel - text: qsTr("Save Chat Context") - helpText: qsTr("Save the chat model's state to disk for faster loading. WARNING: Uses ~2GB per chat.") - Layout.row: 12 - Layout.column: 0 - } - MyCheckBox { - id: saveChatsContextBox - Layout.row: 12 - Layout.column: 2 - Layout.alignment: Qt.AlignRight - checked: MySettings.saveChatsContext - onClicked: { - MySettings.saveChatsContext = !MySettings.saveChatsContext - } - } MySettingsLabel { id: trayLabel text: qsTr("Enable System Tray") diff --git a/gpt4all-chat/qml/ChatCollapsibleItem.qml b/gpt4all-chat/qml/ChatCollapsibleItem.qml new file mode 100644 index 000000000000..459b8da1028f --- /dev/null +++ b/gpt4all-chat/qml/ChatCollapsibleItem.qml @@ -0,0 +1,166 @@ +import Qt5Compat.GraphicalEffects +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import QtQuick.Layouts + +import gpt4all +import mysettings +import toolenums + +ColumnLayout { + property alias textContent: innerTextItem.textContent + property bool isCurrent: false + property bool isError: false + property bool isThinking: false + property int thinkingTime: 0 + + Layout.topMargin: 10 + Layout.bottomMargin: 10 + + Item { + Layout.preferredWidth: childrenRect.width + Layout.preferredHeight: 38 + RowLayout { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + + Item { + Layout.preferredWidth: myTextArea.implicitWidth + Layout.preferredHeight: myTextArea.implicitHeight + TextArea { + id: myTextArea + text: { + if (isError) + return qsTr("Analysis encountered error"); + if (isCurrent) + return isThinking ? qsTr("Thinking") : qsTr("Analyzing"); + return isThinking + ? qsTr("Thought for %1 %2") + .arg(Math.ceil(thinkingTime / 1000.0)) + .arg(Math.ceil(thinkingTime / 1000.0) === 1 ? qsTr("second") : qsTr("seconds")) + : qsTr("Analyzed"); + } + padding: 0 + font.pixelSize: theme.fontSizeLarger + enabled: false + focus: false + readOnly: true + color: headerMA.containsMouse ? theme.mutedDarkTextColorHovered : theme.mutedTextColor + hoverEnabled: false + } + + Item { + id: textColorOverlay + anchors.fill: parent + clip: true + visible: false + Rectangle { + id: animationRec + width: myTextArea.width * 0.3 + anchors.top: parent.top + anchors.bottom: parent.bottom + color: theme.textColor + + SequentialAnimation { + running: isCurrent + loops: Animation.Infinite + NumberAnimation { + target: animationRec; + property: "x"; + from: -animationRec.width; + to: myTextArea.width * 3; + duration: 2000 + } + } + } + } + OpacityMask { + visible: isCurrent + anchors.fill: parent + maskSource: myTextArea + source: textColorOverlay + } + } + + Item { + id: caret + Layout.preferredWidth: contentCaret.width + Layout.preferredHeight: contentCaret.height + Image { + id: contentCaret + anchors.centerIn: parent + visible: false + sourceSize.width: theme.fontSizeLarge + sourceSize.height: theme.fontSizeLarge + mipmap: true + source: { + if (contentLayout.state === "collapsed") + return "qrc:/gpt4all/icons/caret_right.svg"; + else + return "qrc:/gpt4all/icons/caret_down.svg"; + } + } + + ColorOverlay { + anchors.fill: contentCaret + source: contentCaret + color: headerMA.containsMouse ? theme.mutedDarkTextColorHovered : theme.mutedTextColor + } + } + } + + MouseArea { + id: headerMA + hoverEnabled: true + anchors.fill: parent + onClicked: { + if (contentLayout.state === "collapsed") + contentLayout.state = "expanded"; + else + contentLayout.state = "collapsed"; + } + } + } + + ColumnLayout { + id: contentLayout + spacing: 0 + state: "collapsed" + clip: true + + states: [ + State { + name: "expanded" + PropertyChanges { target: contentLayout; Layout.preferredHeight: innerContentLayout.height } + }, + State { + name: "collapsed" + PropertyChanges { target: contentLayout; Layout.preferredHeight: 0 } + } + ] + + transitions: [ + Transition { + SequentialAnimation { + PropertyAnimation { + target: contentLayout + property: "Layout.preferredHeight" + duration: 300 + easing.type: Easing.InOutQuad + } + } + } + ] + + ColumnLayout { + id: innerContentLayout + Layout.leftMargin: 30 + ChatTextItem { + id: innerTextItem + } + } + } +} diff --git a/gpt4all-chat/qml/ChatItemView.qml b/gpt4all-chat/qml/ChatItemView.qml index 8a0c04f8f256..90e09bcbd80d 100644 --- a/gpt4all-chat/qml/ChatItemView.qml +++ b/gpt4all-chat/qml/ChatItemView.qml @@ -4,12 +4,29 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Basic import QtQuick.Layouts +import Qt.labs.qmlmodels import gpt4all import mysettings +import toolenums + +ColumnLayout { + +property var inputBoxText: null +signal setInputBoxText(text: string) + +Item { + +Layout.fillWidth: true +Layout.maximumWidth: parent.width +Layout.preferredHeight: gridLayout.height + +HoverHandler { id: hoverArea } GridLayout { - rows: 5 + id: gridLayout + anchors.left: parent.left + anchors.right: parent.right columns: 2 Item { @@ -18,6 +35,8 @@ GridLayout { Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.preferredWidth: 32 Layout.preferredHeight: 32 + Layout.topMargin: model.index > 0 ? 25 : 0 + Image { id: logo sourceSize: Qt.size(32, 32) @@ -40,7 +59,7 @@ GridLayout { to: 360 duration: 1000 loops: Animation.Infinite - running: currentResponse && (currentChat.responseInProgress || currentChat.restoringFromText) + running: isCurrentResponse && currentChat.responseInProgress } } } @@ -50,6 +69,8 @@ GridLayout { Layout.column: 1 Layout.fillWidth: true Layout.preferredHeight: 38 + Layout.topMargin: model.index > 0 ? 25 : 0 + RowLayout { spacing: 5 anchors.left: parent.left @@ -57,7 +78,11 @@ GridLayout { anchors.bottom: parent.bottom TextArea { - text: name === "Response: " ? qsTr("GPT4All") : qsTr("You") + text: { + if (name === "Response: ") + return qsTr("GPT4All"); + return qsTr("You"); + } padding: 0 font.pixelSize: theme.fontSizeLarger font.bold: true @@ -73,13 +98,11 @@ GridLayout { color: theme.mutedTextColor } RowLayout { - visible: currentResponse && ((value === "" && currentChat.responseInProgress) || currentChat.restoringFromText) + visible: isCurrentResponse && (content === "" && currentChat.responseInProgress) Text { color: theme.mutedTextColor font.pixelSize: theme.fontSizeLarger text: { - if (currentChat.restoringFromText) - return qsTr("restoring from text ..."); switch (currentChat.responseState) { case Chat.ResponseStopped: return qsTr("response stopped ..."); case Chat.LocalDocsRetrieval: return qsTr("retrieving localdocs: %1 ...").arg(currentChat.collectionList.join(", ")); @@ -87,6 +110,7 @@ GridLayout { case Chat.PromptProcessing: return qsTr("processing ...") case Chat.ResponseGeneration: return qsTr("generating response ..."); case Chat.GeneratingQuestions: return qsTr("generating questions ..."); + case Chat.ToolCallGeneration: return qsTr("generating toolcall ..."); default: return ""; // handle unexpected values } } @@ -99,10 +123,11 @@ GridLayout { Layout.row: 1 Layout.column: 1 Layout.fillWidth: true - spacing: 20 + spacing: 10 Flow { id: attachedUrlsFlow Layout.fillWidth: true + Layout.bottomMargin: 10 spacing: 10 visible: promptAttachments.length !== 0 Repeater { @@ -142,125 +167,47 @@ GridLayout { } } - TextArea { - id: myTextArea - Layout.fillWidth: true - padding: 0 - color: { - if (!currentChat.isServer) - return theme.textColor - return theme.white - } - wrapMode: Text.WordWrap - textFormat: TextEdit.PlainText - focus: false - readOnly: true - font.pixelSize: theme.fontSizeLarge - cursorVisible: currentResponse ? currentChat.responseInProgress : false - cursorPosition: text.length - TapHandler { - id: tapHandler - onTapped: function(eventPoint, button) { - var clickedPos = myTextArea.positionAt(eventPoint.position.x, eventPoint.position.y); - var success = textProcessor.tryCopyAtPosition(clickedPos); - if (success) - copyCodeMessage.open(); - } - } - - MouseArea { - id: conversationMouseArea - anchors.fill: parent - acceptedButtons: Qt.RightButton - - onClicked: (mouse) => { - if (mouse.button === Qt.RightButton) { - conversationContextMenu.x = conversationMouseArea.mouseX - conversationContextMenu.y = conversationMouseArea.mouseY - conversationContextMenu.open() - } - } - } - - onLinkActivated: function(link) { - if (!currentResponse || !currentChat.responseInProgress) - Qt.openUrlExternally(link) - } - - onLinkHovered: function (link) { - if (!currentResponse || !currentChat.responseInProgress) - statusBar.externalHoveredLink = link - } - - MyMenu { - id: conversationContextMenu - MyMenuItem { - text: qsTr("Copy") - enabled: myTextArea.selectedText !== "" - height: enabled ? implicitHeight : 0 - onTriggered: myTextArea.copy() - } - MyMenuItem { - text: qsTr("Copy Message") - enabled: myTextArea.selectedText === "" - height: enabled ? implicitHeight : 0 - onTriggered: { - myTextArea.selectAll() - myTextArea.copy() - myTextArea.deselect() + Repeater { + model: childItems + + DelegateChooser { + id: chooser + role: "name" + DelegateChoice { + roleValue: "Text: "; + ChatTextItem { + Layout.fillWidth: true + textContent: modelData.content } } - MyMenuItem { - text: textProcessor.shouldProcessText ? qsTr("Disable markdown") : qsTr("Enable markdown") - height: enabled ? implicitHeight : 0 - onTriggered: { - textProcessor.shouldProcessText = !textProcessor.shouldProcessText; - textProcessor.setValue(value); + DelegateChoice { + roleValue: "ToolCall: "; + ChatCollapsibleItem { + Layout.fillWidth: true + textContent: modelData.content + isCurrent: modelData.isCurrentResponse + isError: modelData.isToolCallError } } - } - - ChatViewTextProcessor { - id: textProcessor - } - - function resetChatViewTextProcessor() { - textProcessor.fontPixelSize = myTextArea.font.pixelSize - textProcessor.codeColors.defaultColor = theme.codeDefaultColor - textProcessor.codeColors.keywordColor = theme.codeKeywordColor - textProcessor.codeColors.functionColor = theme.codeFunctionColor - textProcessor.codeColors.functionCallColor = theme.codeFunctionCallColor - textProcessor.codeColors.commentColor = theme.codeCommentColor - textProcessor.codeColors.stringColor = theme.codeStringColor - textProcessor.codeColors.numberColor = theme.codeNumberColor - textProcessor.codeColors.headerColor = theme.codeHeaderColor - textProcessor.codeColors.backgroundColor = theme.codeBackgroundColor - textProcessor.textDocument = textDocument - textProcessor.setValue(value); - } - - Component.onCompleted: { - resetChatViewTextProcessor(); - chatModel.valueChanged.connect(function(i, value) { - if (index === i) - textProcessor.setValue(value); + DelegateChoice { + roleValue: "Think: "; + ChatCollapsibleItem { + Layout.fillWidth: true + textContent: modelData.content + isCurrent: modelData.isCurrentResponse + isError: false + isThinking: true + thinkingTime: modelData.thinkingTime + } } - ); } - Connections { - target: MySettings - function onFontSizeChanged() { - myTextArea.resetChatViewTextProcessor(); - } - function onChatThemeChanged() { - myTextArea.resetChatViewTextProcessor(); - } - } + delegate: chooser + } - Accessible.role: Accessible.Paragraph - Accessible.name: text - Accessible.description: name === "Response: " ? "The response by the model" : "The prompt by the user" + ChatTextItem { + Layout.fillWidth: true + textContent: content } ThumbsDownDialog { @@ -269,80 +216,19 @@ GridLayout { y: Math.round((parent.height - height) / 2) width: 640 height: 300 - property string text: value + property string text: content response: newResponse === undefined || newResponse === "" ? text : newResponse onAccepted: { var responseHasChanged = response !== text && response !== newResponse if (thumbsDownState && !thumbsUpState && !responseHasChanged) return - chatModel.updateNewResponse(index, response) - chatModel.updateThumbsUpState(index, false) - chatModel.updateThumbsDownState(index, true) + chatModel.updateNewResponse(model.index, response) + chatModel.updateThumbsUpState(model.index, false) + chatModel.updateThumbsDownState(model.index, true) Network.sendConversation(currentChat.id, getConversationJson()); } } - - Column { - Layout.alignment: Qt.AlignRight - Layout.rightMargin: 15 - visible: name === "Response: " && - (!currentResponse || !currentChat.responseInProgress) && MySettings.networkIsActive - spacing: 10 - - Item { - width: childrenRect.width - height: childrenRect.height - MyToolButton { - id: thumbsUp - width: 24 - height: 24 - imageWidth: width - imageHeight: height - opacity: thumbsUpState || thumbsUpState == thumbsDownState ? 1.0 : 0.2 - source: "qrc:/gpt4all/icons/thumbs_up.svg" - Accessible.name: qsTr("Thumbs up") - Accessible.description: qsTr("Gives a thumbs up to the response") - onClicked: { - if (thumbsUpState && !thumbsDownState) - return - - chatModel.updateNewResponse(index, "") - chatModel.updateThumbsUpState(index, true) - chatModel.updateThumbsDownState(index, false) - Network.sendConversation(currentChat.id, getConversationJson()); - } - } - - MyToolButton { - id: thumbsDown - anchors.top: thumbsUp.top - anchors.topMargin: 3 - anchors.left: thumbsUp.right - anchors.leftMargin: 3 - width: 24 - height: 24 - imageWidth: width - imageHeight: height - checked: thumbsDownState - opacity: thumbsDownState || thumbsUpState == thumbsDownState ? 1.0 : 0.2 - transform: [ - Matrix4x4 { - matrix: Qt.matrix4x4(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) - }, - Translate { - x: thumbsDown.width - } - ] - source: "qrc:/gpt4all/icons/thumbs_down.svg" - Accessible.name: qsTr("Thumbs down") - Accessible.description: qsTr("Opens thumbs down dialog") - onClicked: { - thumbsDownDialog.open() - } - } - } - } } Item { @@ -353,11 +239,13 @@ GridLayout { Layout.preferredWidth: childrenRect.width Layout.preferredHeight: childrenRect.height visible: { + if (name !== "Response: ") + return false if (consolidatedSources.length === 0) return false if (!MySettings.localDocsShowReferences) return false - if (currentResponse && currentChat.responseInProgress + if (isCurrentResponse && currentChat.responseInProgress && currentChat.responseState !== Chat.GeneratingQuestions ) return false return true @@ -443,7 +331,7 @@ GridLayout { return false if (!MySettings.localDocsShowReferences) return false - if (currentResponse && currentChat.responseInProgress + if (isCurrentResponse && currentChat.responseInProgress && currentChat.responseState !== Chat.GeneratingQuestions ) return false return true @@ -455,7 +343,7 @@ GridLayout { states: [ State { name: "expanded" - PropertyChanges { target: sourcesLayout; Layout.preferredHeight: flow.height } + PropertyChanges { target: sourcesLayout; Layout.preferredHeight: sourcesFlow.height } }, State { name: "collapsed" @@ -477,7 +365,7 @@ GridLayout { ] Flow { - id: flow + id: sourcesFlow Layout.fillWidth: true spacing: 10 visible: consolidatedSources.length !== 0 @@ -566,8 +454,159 @@ GridLayout { } } + ConfirmationDialog { + id: editPromptDialog + dialogTitle: qsTr("Edit this message?") + description: qsTr("All following messages will be permanently erased.") + onAccepted: { + const msg = currentChat.popPrompt(index); + if (msg !== null) + setInputBoxText(msg); + } + } + + ConfirmationDialog { + id: redoResponseDialog + dialogTitle: qsTr("Redo this response?") + description: qsTr("All following messages will be permanently erased.") + onAccepted: currentChat.regenerateResponse(index) + } + + RowLayout { + id: buttonRow + Layout.row: 4 + Layout.column: 1 + Layout.maximumWidth: parent.width + Layout.fillWidth: false + Layout.alignment: Qt.AlignLeft | Qt.AlignTop + spacing: 3 + visible: !isCurrentResponse || !currentChat.responseInProgress + enabled: opacity > 0 + opacity: hoverArea.hovered + + Behavior on opacity { + OpacityAnimator { duration: 30 } + } + + ChatMessageButton { + readonly property var editingDisabledReason: { + if (!currentChat.isModelLoaded) + return qsTr("Cannot edit chat without a loaded model."); + if (currentChat.responseInProgress) + return qsTr("Cannot edit chat while the model is generating."); + return null; + } + visible: !currentChat.isServer && model.name === "Prompt: " + enabled: editingDisabledReason === null + Layout.maximumWidth: 24 + Layout.maximumHeight: 24 + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: false + name: editingDisabledReason ?? qsTr("Edit") + source: "qrc:/gpt4all/icons/edit.svg" + onClicked: { + if (inputBoxText === "") + editPromptDialog.open(); + } + } + + ChatMessageButton { + readonly property var editingDisabledReason: { + if (!currentChat.isModelLoaded) + return qsTr("Cannot redo response without a loaded model."); + if (currentChat.responseInProgress) + return qsTr("Cannot redo response while the model is generating."); + return null; + } + visible: !currentChat.isServer && model.name === "Response: " + enabled: editingDisabledReason === null + Layout.maximumWidth: 24 + Layout.maximumHeight: 24 + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: false + name: editingDisabledReason ?? qsTr("Redo") + source: "qrc:/gpt4all/icons/regenerate.svg" + onClicked: { + if (index == chatModel.count - 1) { + // regenerate last message without confirmation + currentChat.regenerateResponse(index); + return; + } + redoResponseDialog.open(); + } + } + + ChatMessageButton { + Layout.maximumWidth: 24 + Layout.maximumHeight: 24 + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: false + name: qsTr("Copy") + source: "qrc:/gpt4all/icons/copy.svg" + onClicked: { + chatModel.copyToClipboard(index); + } + } + + Item { + visible: name === "Response: " && MySettings.networkIsActive + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: childrenRect.width + Layout.preferredHeight: childrenRect.height + Layout.fillWidth: false + + ChatMessageButton { + id: thumbsUp + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + opacity: thumbsUpState || thumbsUpState == thumbsDownState ? 1.0 : 0.2 + source: "qrc:/gpt4all/icons/thumbs_up.svg" + name: qsTr("Like response") + onClicked: { + if (thumbsUpState && !thumbsDownState) + return + + chatModel.updateNewResponse(index, "") + chatModel.updateThumbsUpState(index, true) + chatModel.updateThumbsDownState(index, false) + Network.sendConversation(currentChat.id, getConversationJson()); + } + } + + ChatMessageButton { + id: thumbsDown + anchors.top: thumbsUp.top + anchors.topMargin: buttonRow.spacing + anchors.left: thumbsUp.right + anchors.leftMargin: buttonRow.spacing + checked: thumbsDownState + opacity: thumbsDownState || thumbsUpState == thumbsDownState ? 1.0 : 0.2 + bgTransform: [ + Matrix4x4 { + matrix: Qt.matrix4x4(-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) + }, + Translate { + x: thumbsDown.width + } + ] + source: "qrc:/gpt4all/icons/thumbs_down.svg" + name: qsTr("Dislike response") + onClicked: { + thumbsDownDialog.open() + } + } + } + } +} // GridLayout + +} // Item + +GridLayout { + Layout.fillWidth: true + Layout.maximumWidth: parent.width + function shouldShowSuggestions() { - if (!currentResponse) + if (!isCurrentResponse) return false; if (MySettings.suggestionMode === 2) // Off return false; @@ -577,8 +616,8 @@ GridLayout { } Item { - visible: shouldShowSuggestions() - Layout.row: 4 + visible: parent.shouldShowSuggestions() + Layout.row: 5 Layout.column: 0 Layout.topMargin: 20 Layout.alignment: Qt.AlignVCenter | Qt.AlignRight @@ -601,8 +640,8 @@ GridLayout { } Item { - visible: shouldShowSuggestions() - Layout.row: 4 + visible: parent.shouldShowSuggestions() + Layout.row: 5 Layout.column: 1 Layout.topMargin: 20 Layout.fillWidth: true @@ -627,8 +666,8 @@ GridLayout { } ColumnLayout { - visible: shouldShowSuggestions() - Layout.row: 5 + visible: parent.shouldShowSuggestions() + Layout.row: 6 Layout.column: 1 Layout.fillWidth: true Layout.minimumHeight: 1 @@ -786,4 +825,7 @@ GridLayout { } } } -} + +} // GridLayout + +} // ColumnLayout diff --git a/gpt4all-chat/qml/ChatMessageButton.qml b/gpt4all-chat/qml/ChatMessageButton.qml new file mode 100644 index 000000000000..b3c0a31a7f1e --- /dev/null +++ b/gpt4all-chat/qml/ChatMessageButton.qml @@ -0,0 +1,20 @@ +import QtQuick +import QtQuick.Controls + +import gpt4all + +MyToolButton { + property string name + + width: 24 + height: 24 + imageWidth: width + imageHeight: height + ToolTip { + visible: parent.hovered + y: parent.height * 1.5 + text: name + delay: Qt.styleHints.mousePressAndHoldInterval + } + Accessible.name: name +} diff --git a/gpt4all-chat/qml/ChatTextItem.qml b/gpt4all-chat/qml/ChatTextItem.qml new file mode 100644 index 000000000000..ca4c2fa2ff6e --- /dev/null +++ b/gpt4all-chat/qml/ChatTextItem.qml @@ -0,0 +1,139 @@ +import Qt5Compat.GraphicalEffects +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import QtQuick.Layouts + +import gpt4all +import mysettings +import toolenums + +TextArea { + id: myTextArea + property string textContent: "" + visible: textContent != "" + Layout.fillWidth: true + padding: 0 + color: { + if (!currentChat.isServer) + return theme.textColor + return theme.white + } + wrapMode: Text.WordWrap + textFormat: TextEdit.PlainText + focus: false + readOnly: true + font.pixelSize: theme.fontSizeLarge + cursorVisible: isCurrentResponse ? currentChat.responseInProgress : false + cursorPosition: text.length + TapHandler { + id: tapHandler + onTapped: function(eventPoint, button) { + var clickedPos = myTextArea.positionAt(eventPoint.position.x, eventPoint.position.y); + var success = textProcessor.tryCopyAtPosition(clickedPos); + if (success) + copyCodeMessage.open(); + } + } + + MouseArea { + id: conversationMouseArea + anchors.fill: parent + acceptedButtons: Qt.RightButton + + onClicked: (mouse) => { + if (mouse.button === Qt.RightButton) { + conversationContextMenu.x = conversationMouseArea.mouseX + conversationContextMenu.y = conversationMouseArea.mouseY + conversationContextMenu.open() + } + } + } + + onLinkActivated: function(link) { + if (!isCurrentResponse || !currentChat.responseInProgress) + Qt.openUrlExternally(link) + } + + onLinkHovered: function (link) { + if (!isCurrentResponse || !currentChat.responseInProgress) + statusBar.externalHoveredLink = link + } + + MyMenu { + id: conversationContextMenu + MyMenuItem { + text: qsTr("Copy") + enabled: myTextArea.selectedText !== "" + height: enabled ? implicitHeight : 0 + onTriggered: myTextArea.copy() + } + MyMenuItem { + text: qsTr("Copy Message") + enabled: myTextArea.selectedText === "" + height: enabled ? implicitHeight : 0 + onTriggered: { + myTextArea.selectAll() + myTextArea.copy() + myTextArea.deselect() + } + } + MyMenuItem { + text: textProcessor.shouldProcessText ? qsTr("Disable markdown") : qsTr("Enable markdown") + height: enabled ? implicitHeight : 0 + onTriggered: { + textProcessor.shouldProcessText = !textProcessor.shouldProcessText; + textProcessor.setValue(textContent); + } + } + } + + ChatViewTextProcessor { + id: textProcessor + } + + function resetChatViewTextProcessor() { + textProcessor.fontPixelSize = myTextArea.font.pixelSize + textProcessor.codeColors.defaultColor = theme.codeDefaultColor + textProcessor.codeColors.keywordColor = theme.codeKeywordColor + textProcessor.codeColors.functionColor = theme.codeFunctionColor + textProcessor.codeColors.functionCallColor = theme.codeFunctionCallColor + textProcessor.codeColors.commentColor = theme.codeCommentColor + textProcessor.codeColors.stringColor = theme.codeStringColor + textProcessor.codeColors.numberColor = theme.codeNumberColor + textProcessor.codeColors.headerColor = theme.codeHeaderColor + textProcessor.codeColors.backgroundColor = theme.codeBackgroundColor + textProcessor.textDocument = textDocument + textProcessor.setValue(textContent); + } + + property bool textProcessorReady: false + + Component.onCompleted: { + resetChatViewTextProcessor(); + textProcessorReady = true; + } + + Connections { + target: myTextArea + function onTextContentChanged() { + if (myTextArea.textProcessorReady) + textProcessor.setValue(textContent); + } + } + + Connections { + target: MySettings + function onFontSizeChanged() { + myTextArea.resetChatViewTextProcessor(); + } + function onChatThemeChanged() { + myTextArea.resetChatViewTextProcessor(); + } + } + + Accessible.role: Accessible.Paragraph + Accessible.name: text + Accessible.description: name === "Response: " ? "The response by the model" : "The prompt by the user" +} diff --git a/gpt4all-chat/qml/ChatView.qml b/gpt4all-chat/qml/ChatView.qml index 219a5139801e..12f6adad1bdf 100644 --- a/gpt4all-chat/qml/ChatView.qml +++ b/gpt4all-chat/qml/ChatView.qml @@ -24,6 +24,12 @@ Rectangle { property var currentChat: ChatListModel.currentChat property var chatModel: currentChat.chatModel + property var currentModelInfo: currentChat && currentChat.modelInfo + property var currentModelId: null + onCurrentModelInfoChanged: { + const newId = currentModelInfo && currentModelInfo.id; + if (currentModelId !== newId) { currentModelId = newId; } + } signal addCollectionViewRequested() signal addModelViewRequested() @@ -31,10 +37,11 @@ Rectangle { Connections { target: currentChat - function onResponseInProgressChanged() { - if (MySettings.networkIsActive && !currentChat.responseInProgress) - Network.sendConversation(currentChat.id, getConversationJson()); - } + // FIXME: https://github.com/nomic-ai/gpt4all/issues/3334 + // function onResponseInProgressChanged() { + // if (MySettings.networkIsActive && !currentChat.responseInProgress) + // Network.sendConversation(currentChat.id, getConversationJson()); + // } function onModelLoadingErrorChanged() { if (currentChat.modelLoadingError !== "") modelLoadingErrorPopup.open() @@ -79,14 +86,11 @@ Rectangle { function open_(msg) { message = msg; open(); } } - SwitchModelDialog { + ConfirmationDialog { id: switchModelDialog - anchors.centerIn: parent - Item { - Accessible.role: Accessible.Dialog - Accessible.name: qsTr("Switch model dialog") - Accessible.description: qsTr("Warn the user if they switch models, then context will be erased") - } + property int index: -1 + dialogTitle: qsTr("Erase conversation?") + description: qsTr("Changing the model will erase the current conversation.") } PopupDialog { @@ -103,42 +107,54 @@ Rectangle { font.pixelSize: theme.fontSizeLarge } - function getConversation() { - var conversation = ""; - for (var i = 0; i < chatModel.count; i++) { - var item = chatModel.get(i) - var string = item.name; - var isResponse = item.name === "Response: " - string += chatModel.get(i).value - if (isResponse && item.stopped) - string += " " - string += "\n" - conversation += string + ConfirmationDialog { + id: resetContextDialog + dialogTitle: qsTr("Erase conversation?") + description: qsTr("The entire chat will be erased.") + onAccepted: { + Network.trackChatEvent("reset_context", { "length": chatModel.count }); + currentChat.reset(); } - return conversation } - function getConversationJson() { - var str = "{\"conversation\": ["; - for (var i = 0; i < chatModel.count; i++) { - var item = chatModel.get(i) - var isResponse = item.name === "Response: " - str += "{\"content\": "; - str += JSON.stringify(item.value) - str += ", \"role\": \"" + (isResponse ? "assistant" : "user") + "\""; - if (isResponse && item.thumbsUpState !== item.thumbsDownState) - str += ", \"rating\": \"" + (item.thumbsUpState ? "positive" : "negative") + "\""; - if (isResponse && item.newResponse !== "") - str += ", \"edited_content\": " + JSON.stringify(item.newResponse); - if (isResponse && item.stopped) - str += ", \"stopped\": \"true\"" - if (!isResponse) - str += "}," - else - str += ((i < chatModel.count - 1) ? "}," : "}") - } - return str + "]}" - } + // FIXME: https://github.com/nomic-ai/gpt4all/issues/3334 + // function getConversation() { + // var conversation = ""; + // for (var i = 0; i < chatModel.count; i++) { + // var item = chatModel.get(i) + // var string = item.name; + // var isResponse = item.name === "Response: " + // string += chatModel.get(i).value + // if (isResponse && item.stopped) + // string += " " + // string += "\n" + // conversation += string + // } + // return conversation + // } + + // FIXME: https://github.com/nomic-ai/gpt4all/issues/3334 + // function getConversationJson() { + // var str = "{\"conversation\": ["; + // for (var i = 0; i < chatModel.count; i++) { + // var item = chatModel.get(i) + // var isResponse = item.name === "Response: " + // str += "{\"content\": "; + // str += JSON.stringify(item.value) + // str += ", \"role\": \"" + (isResponse ? "assistant" : "user") + "\""; + // if (isResponse && item.thumbsUpState !== item.thumbsDownState) + // str += ", \"rating\": \"" + (item.thumbsUpState ? "positive" : "negative") + "\""; + // if (isResponse && item.newResponse !== "") + // str += ", \"edited_content\": " + JSON.stringify(item.newResponse); + // if (isResponse && item.stopped) + // str += ", \"stopped\": \"true\"" + // if (!isResponse) + // str += "}," + // else + // str += ((i < chatModel.count - 1) ? "}," : "}") + // } + // return str + "]}" + // } ChatDrawer { id: chatDrawer @@ -655,6 +671,7 @@ Rectangle { id: homePage color: "transparent" anchors.fill: parent + z: 200 visible: !currentChat.isModelLoaded && (ModelList.selectableModels.count === 0 || currentModelName() === "") && !currentChat.isServer ColumnLayout { @@ -703,7 +720,7 @@ Rectangle { if (i !== -1) { defaultModel = comboBox.valueAt(i); } else { - defaultModel = comboBox.valueAt(0); + defaultModel = comboBox.count ? comboBox.valueAt(0) : ""; } if (defaultModel !== "") { defaultModelName = ModelList.modelInfo(defaultModel).name; @@ -780,7 +797,7 @@ Rectangle { ColumnLayout { anchors.fill: parent - visible: ModelList.selectableModels.count !== 0 && chatModel.count !== 0 + visible: ModelList.selectableModels.count !== 0 ListView { id: listView Layout.maximumWidth: 1280 @@ -790,9 +807,9 @@ Rectangle { Layout.leftMargin: 50 Layout.rightMargin: 50 Layout.alignment: Qt.AlignHCenter - spacing: 25 + spacing: 10 model: chatModel - cacheBuffer: Math.max(0, listView.contentHeight) + cacheBuffer: 2147483647 ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded @@ -804,6 +821,18 @@ Rectangle { delegate: ChatItemView { width: listView.contentItem.width - 15 + inputBoxText: textInput.text + onSetInputBoxText: text => { + textInput.text = text; + textInput.forceActiveFocus(); + textInput.cursorPosition = text.length; + } + height: visible ? implicitHeight : 0 + visible: name !== "ToolResponse: " && name !== "System: " + } + + remove: Transition { + OpacityAnimator { to: 0; duration: 500 } } function scrollToEnd() { @@ -832,11 +861,9 @@ Rectangle { clip: true z: 400 - property bool isHovered: { - return conversationTrayButton.isHovered || - resetContextButton.hovered || copyChatButton.hovered || - regenerateButton.hovered - } + property bool isHovered: ( + conversationTrayButton.isHovered || resetContextButton.hovered || copyChatButton.hovered + ) state: conversationTrayContent.isHovered ? "expanded" : "collapsed" states: [ @@ -892,11 +919,7 @@ Rectangle { source: "qrc:/gpt4all/icons/recycle.svg" imageWidth: 20 imageHeight: 20 - onClicked: { - Network.trackChatEvent("reset_context", { "length": chatModel.count }) - currentChat.reset(); - currentChat.processSystemPrompt(); - } + onClicked: resetContextDialog.open() ToolTip.visible: resetContextButton.hovered ToolTip.text: qsTr("Erase and reset chat session") } @@ -912,43 +935,12 @@ Rectangle { visible: false } onClicked: { - var conversation = getConversation() - copyEdit.text = conversation - copyEdit.selectAll() - copyEdit.copy() + chatModel.copyToClipboard() copyMessage.open() } ToolTip.visible: copyChatButton.hovered ToolTip.text: qsTr("Copy chat session to clipboard") } - MyToolButton { - id: regenerateButton - Layout.preferredWidth: 40 - Layout.preferredHeight: 40 - source: "qrc:/gpt4all/icons/regenerate.svg" - imageWidth: 20 - imageHeight: 20 - visible: chatModel.count && !currentChat.isServer && currentChat.isModelLoaded && !currentChat.responseInProgress - onClicked: { - if (chatModel.count < 2) - return - var promptIndex = chatModel.count - 2 - var promptElement = chatModel.get(promptIndex) - var responseIndex = chatModel.count - 1 - var responseElement = chatModel.get(responseIndex) - if (promptElement.name !== "Prompt: " || responseElement.name !== "Response: ") - return - currentChat.regenerateResponse() - chatModel.updateCurrentResponse(responseIndex, true) - chatModel.updateStopped(responseIndex, false) - chatModel.updateThumbsUpState(responseIndex, false) - chatModel.updateThumbsDownState(responseIndex, false) - chatModel.updateNewResponse(responseIndex, "") - currentChat.prompt(promptElement.promptPlusAttachments) - } - ToolTip.visible: regenerateButton.hovered - ToolTip.text: qsTr("Redo last chat response") - } } } @@ -1026,13 +1018,15 @@ Rectangle { anchors.leftMargin: 30 horizontalAlignment: Qt.AlignRight verticalAlignment: Qt.AlignVCenter - color: theme.mutedTextColor - visible: currentChat.tokenSpeed !== "" || externalHoveredLink !== "" + color: textInputView.error !== null ? theme.textErrorColor : theme.mutedTextColor + visible: currentChat.tokenSpeed !== "" || externalHoveredLink !== "" || textInputView.error !== null elide: Text.ElideRight wrapMode: Text.WordWrap text: { if (externalHoveredLink !== "") return externalHoveredLink + if (textInputView.error !== null) + return textInputView.error; const segments = [currentChat.tokenSpeed]; const device = currentChat.device; @@ -1050,6 +1044,7 @@ Rectangle { } font.pixelSize: theme.fontSizeSmaller font.bold: true + onLinkActivated: function(link) { Qt.openUrlExternally(link) } } RectangularGlow { @@ -1079,8 +1074,8 @@ Rectangle { Rectangle { id: textInputView color: theme.controlBackground - border.width: 1 - border.color: theme.controlBorder + border.width: error === null ? 1 : 2 + border.color: error === null ? theme.controlBorder : theme.textErrorColor radius: 10 anchors.left: parent.left anchors.right: parent.right @@ -1091,6 +1086,41 @@ Rectangle { height: textInputViewLayout.implicitHeight visible: !currentChat.isServer && ModelList.selectableModels.count !== 0 + property var error: null + function checkError() { + const info = currentModelInfo; + if (info === null || !info.id) { + error = null; + } else if (info.chatTemplate.isLegacy) { + error = qsTr("Legacy prompt template needs to be " + + "updated" + + " in Settings."); + } else if (!info.chatTemplate.isSet) { + error = qsTr("No " + + "chat template configured."); + } else if (/^\s*$/.test(info.chatTemplate.value)) { + error = qsTr("The " + + "chat template cannot be blank."); + } else if (info.systemMessage.isLegacy) { + error = qsTr("Legacy system prompt needs to be " + + "updated" + + " in Settings."); + } else + error = null; + } + Component.onCompleted: checkError() + Connections { + target: window + function onCurrentModelIdChanged() { textInputView.checkError(); } + } + Connections { + target: MySettings + function onChatTemplateChanged(info) + { if (info.id === window.currentModelId) textInputView.checkError(); } + function onSystemMessageChanged(info) + { if (info.id === window.currentModelId) textInputView.checkError(); } + } + MouseArea { id: textInputViewMouseArea anchors.fill: parent @@ -1214,16 +1244,16 @@ Rectangle { Accessible.role: Accessible.EditableText Accessible.name: placeholderText Accessible.description: qsTr("Send messages/prompts to the model") - Keys.onReturnPressed: (event)=> { - if (event.modifiers & Qt.ControlModifier || event.modifiers & Qt.ShiftModifier) - event.accepted = false; - else { - editingFinished(); - sendMessage() - } - } + Keys.onReturnPressed: event => { + if (event.modifiers & Qt.ControlModifier || event.modifiers & Qt.ShiftModifier) { + event.accepted = false; + } else if (!chatModel.hasError && textInputView.error === null) { + editingFinished(); + sendMessage(); + } + } function sendMessage() { - if ((textInput.text === "" && attachmentModel.count === 0) || currentChat.responseInProgress || currentChat.restoringFromText) + if ((textInput.text === "" && attachmentModel.count === 0) || currentChat.responseInProgress) return currentChat.stopGenerating() @@ -1324,9 +1354,10 @@ Rectangle { ToolTip.text: Accessible.description onClicked: { - var index = Math.max(0, chatModel.count - 1); - var listElement = chatModel.get(index); - listElement.stopped = true + // FIXME: This no longer sets a 'stopped' field so conversations that + // are copied to clipboard or to datalake don't indicate if the user + // has prematurely stopped the response. This has been broken since + // v3.0.0 at least. currentChat.stopGenerating() } } @@ -1338,6 +1369,7 @@ Rectangle { imageWidth: theme.fontSizeLargest imageHeight: theme.fontSizeLargest visible: !currentChat.responseInProgress && !currentChat.isServer && ModelList.selectableModels.count !== 0 + enabled: !chatModel.hasError && textInputView.error === null source: "qrc:/gpt4all/icons/send_message.svg" Accessible.name: qsTr("Send message") Accessible.description: qsTr("Sends the message/prompt contained in textfield to the model") diff --git a/gpt4all-chat/qml/ConfirmationDialog.qml b/gpt4all-chat/qml/ConfirmationDialog.qml new file mode 100644 index 000000000000..4220245320c8 --- /dev/null +++ b/gpt4all-chat/qml/ConfirmationDialog.qml @@ -0,0 +1,59 @@ +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import QtQuick.Layouts + +MyDialog { + id: confirmationDialog + anchors.centerIn: parent + modal: true + padding: 20 + property alias dialogTitle: titleText.text + property alias description: descriptionText.text + + Theme { id: theme } + + contentItem: ColumnLayout { + Text { + id: titleText + Layout.alignment: Qt.AlignHCenter + textFormat: Text.StyledText + color: theme.textColor + font.pixelSize: theme.fontSizeLarger + font.bold: true + } + + Text { + id: descriptionText + Layout.alignment: Qt.AlignHCenter + textFormat: Text.StyledText + color: theme.textColor + font.pixelSize: theme.fontSizeMedium + } + } + + footer: DialogButtonBox { + id: dialogBox + padding: 20 + alignment: Qt.AlignRight + spacing: 10 + MySettingsButton { + text: qsTr("OK") + textColor: theme.mediumButtonText + backgroundColor: theme.mediumButtonBackground + backgroundColorHovered: theme.mediumButtonBackgroundHovered + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + MySettingsButton { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + } + background: Rectangle { + color: "transparent" + } + Keys.onEnterPressed: confirmationDialog.accept() + Keys.onReturnPressed: confirmationDialog.accept() + } + Component.onCompleted: dialogBox.forceActiveFocus() +} diff --git a/gpt4all-chat/qml/HomeView.qml b/gpt4all-chat/qml/HomeView.qml index 69ef16e23f58..8d76d5dbfd46 100644 --- a/gpt4all-chat/qml/HomeView.qml +++ b/gpt4all-chat/qml/HomeView.qml @@ -47,7 +47,7 @@ Rectangle { id: welcome Layout.alignment: Qt.AlignHCenter text: qsTr("Welcome to GPT4All") - font.pixelSize: theme.fontSizeBanner + font.pixelSize: theme.fontSizeBannerLarge color: theme.titleTextColor } diff --git a/gpt4all-chat/qml/LocalDocsSettings.qml b/gpt4all-chat/qml/LocalDocsSettings.qml index a7ea5b75eb41..95124c9c822d 100644 --- a/gpt4all-chat/qml/LocalDocsSettings.qml +++ b/gpt4all-chat/qml/LocalDocsSettings.qml @@ -10,7 +10,7 @@ import mysettings import network MySettingsTab { - onRestoreDefaultsClicked: { + onRestoreDefaults: { MySettings.restoreLocalDocsDefaults(); } diff --git a/gpt4all-chat/qml/ModelSettings.qml b/gpt4all-chat/qml/ModelSettings.qml index 2435e08f8b5d..62906440c936 100644 --- a/gpt4all-chat/qml/ModelSettings.qml +++ b/gpt4all-chat/qml/ModelSettings.qml @@ -8,10 +8,34 @@ import mysettings import chatlistmodel MySettingsTab { - onRestoreDefaultsClicked: { + onRestoreDefaults: { MySettings.restoreModelDefaults(root.currentModelInfo); } title: qsTr("Model") + + ConfirmationDialog { + id: resetSystemMessageDialog + property var index: null + property bool resetClears: false + dialogTitle: qsTr("%1 system message?").arg(resetClears ? qsTr("Clear") : qsTr("Reset")) + description: qsTr("The system message will be %1.").arg(resetClears ? qsTr("removed") : qsTr("reset to the default")) + onAccepted: MySettings.resetModelSystemMessage(ModelList.modelInfo(index)) + function show(index_, resetClears_) { index = index_; resetClears = resetClears_; open(); } + } + + ConfirmationDialog { + id: resetChatTemplateDialog + property bool resetClears: false + property var index: null + dialogTitle: qsTr("%1 chat template?").arg(resetClears ? qsTr("Clear") : qsTr("Reset")) + description: qsTr("The chat template will be %1.").arg(resetClears ? qsTr("erased") : qsTr("reset to the default")) + onAccepted: { + MySettings.resetModelChatTemplate(ModelList.modelInfo(index)); + templateTextArea.resetText(); + } + function show(index_, resetClears_) { index = index_; resetClears = resetClears_; open(); } + } + contentItem: GridLayout { id: root columns: 3 @@ -35,6 +59,7 @@ MySettingsTab { RowLayout { Layout.fillWidth: true + Layout.maximumWidth: parent.width Layout.row: 2 Layout.column: 0 Layout.columnSpan: 2 @@ -153,69 +178,154 @@ MySettingsTab { Layout.fillWidth: true } - MySettingsLabel { - visible: !root.currentModelInfo.isOnline - text: qsTr("System Prompt") - helpText: qsTr("Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens.") + RowLayout { Layout.row: 7 - Layout.column: 0 + Layout.columnSpan: 2 Layout.topMargin: 15 + Layout.fillWidth: true + Layout.maximumWidth: parent.width + spacing: 10 + MySettingsLabel { + id: systemMessageLabel + text: qsTr("System Message") + helpText: qsTr("A message to set the context or guide the behavior of the model. Leave blank for " + + "none. NOTE: Since GPT4All 3.5, this should not contain control tokens.") + onReset: () => resetSystemMessageDialog.show(root.currentModelId, resetClears) + function updateResetButton() { + const info = root.currentModelInfo; + // NOTE: checks if the *override* is set, regardless of whether there is a default + canReset = !!info.id && MySettings.isModelSystemMessageSet(info); + resetClears = !info.defaultSystemMessage; + } + Component.onCompleted: updateResetButton() + Connections { + target: root + function onCurrentModelIdChanged() { systemMessageLabel.updateResetButton(); } + } + Connections { + target: MySettings + function onSystemMessageChanged(info) + { if (info.id === root.currentModelId) systemMessageLabel.updateResetButton(); } + } + } + Label { + id: systemMessageLabelHelp + visible: systemMessageArea.errState !== "ok" + Layout.alignment: Qt.AlignBottom + Layout.fillWidth: true + Layout.rightMargin: 5 + Layout.maximumHeight: systemMessageLabel.height + text: qsTr("System message is not " + + "plain text.") + color: systemMessageArea.errState === "error" ? theme.textErrorColor : theme.textWarningColor + font.pixelSize: theme.fontSizeLarger + font.bold: true + wrapMode: Text.Wrap + elide: Text.ElideRight + onLinkActivated: function(link) { Qt.openUrlExternally(link) } + } } Rectangle { - id: systemPrompt - visible: !root.currentModelInfo.isOnline + id: systemMessage Layout.row: 8 Layout.column: 0 Layout.columnSpan: 2 Layout.fillWidth: true color: "transparent" - Layout.minimumHeight: Math.max(100, systemPromptArea.contentHeight + 20) + Layout.minimumHeight: Math.max(100, systemMessageArea.contentHeight + 20) MyTextArea { - id: systemPromptArea + id: systemMessageArea anchors.fill: parent - text: root.currentModelInfo.systemPrompt + property bool isBeingReset: false + function resetText() { + const info = root.currentModelInfo; + isBeingReset = true; + text = (info.id ? info.systemMessage.value : null) ?? ""; + isBeingReset = false; + } + Component.onCompleted: resetText() Connections { target: MySettings - function onSystemPromptChanged() { - systemPromptArea.text = root.currentModelInfo.systemPrompt; - } + function onSystemMessageChanged(info) + { if (info.id === root.currentModelId) systemMessageArea.resetText(); } } Connections { target: root - function onCurrentModelInfoChanged() { - systemPromptArea.text = root.currentModelInfo.systemPrompt; - } + function onCurrentModelIdChanged() { systemMessageArea.resetText(); } } + // strict validation, because setModelSystemMessage clears isLegacy + readonly property var reLegacyCheck: ( + /(?:^|\s)(?:### *System\b|S(?:ystem|YSTEM):)|<\|(?:im_(?:start|end)|(?:start|end)_header_id|eot_id|SYSTEM_TOKEN)\|>|<>/m + ) onTextChanged: { - MySettings.setModelSystemPrompt(root.currentModelInfo, text) + const info = root.currentModelInfo; + if (!info.id) { + errState = "ok"; + } else if (info.systemMessage.isLegacy && (isBeingReset || reLegacyCheck.test(text))) { + errState = "error"; + } else + errState = reLegacyCheck.test(text) ? "warning" : "ok"; + if (info.id && errState !== "error" && !isBeingReset) + MySettings.setModelSystemMessage(info, text); + systemMessageLabel.updateResetButton(); } Accessible.role: Accessible.EditableText + Accessible.name: systemMessageLabel.text + Accessible.description: systemMessageLabelHelp.text } } RowLayout { Layout.row: 9 - Layout.column: 0 Layout.columnSpan: 2 Layout.topMargin: 15 + Layout.fillWidth: true + Layout.maximumWidth: parent.width spacing: 10 MySettingsLabel { - id: promptTemplateLabel - text: qsTr("Prompt Template") - helpText: qsTr("The template that wraps every prompt.") + id: chatTemplateLabel + text: qsTr("Chat Template") + helpText: qsTr("This Jinja template turns the chat into input for the model.") + onReset: () => resetChatTemplateDialog.show(root.currentModelId, resetClears) + function updateResetButton() { + const info = root.currentModelInfo; + canReset = !!info.id && ( + MySettings.isModelChatTemplateSet(info) + || templateTextArea.text !== (info.chatTemplate.value ?? "") + ); + resetClears = !info.defaultChatTemplate; + } + Component.onCompleted: updateResetButton() + Connections { + target: root + function onCurrentModelIdChanged() { chatTemplateLabel.updateResetButton(); } + } + Connections { + target: MySettings + function onChatTemplateChanged(info) + { if (info.id === root.currentModelId) chatTemplateLabel.updateResetButton(); } + } } - MySettingsLabel { - id: promptTemplateLabelHelp - text: qsTr("Must contain the string \"%1\" to be replaced with the user's input.") - color: theme.textErrorColor - visible: templateTextArea.text.indexOf("%1") === -1 - wrapMode: TextArea.Wrap + Label { + id: chatTemplateLabelHelp + visible: templateTextArea.errState !== "ok" + Layout.alignment: Qt.AlignBottom + Layout.fillWidth: true + Layout.rightMargin: 5 + Layout.maximumHeight: chatTemplateLabel.height + text: templateTextArea.errMsg + color: templateTextArea.errState === "error" ? theme.textErrorColor : theme.textWarningColor + font.pixelSize: theme.fontSizeLarger + font.bold: true + wrapMode: Text.Wrap + elide: Text.ElideRight + onLinkActivated: function(link) { Qt.openUrlExternally(link) } } } Rectangle { - id: promptTemplate + id: chatTemplate Layout.row: 10 Layout.column: 0 Layout.columnSpan: 2 @@ -226,27 +336,71 @@ MySettingsTab { MyTextArea { id: templateTextArea anchors.fill: parent - text: root.currentModelInfo.promptTemplate + font: fixedFont + property bool isBeingReset: false + property var errMsg: null + function resetText() { + const info = root.currentModelInfo; + isBeingReset = true; + text = (info.id ? info.chatTemplate.value : null) ?? ""; + isBeingReset = false; + } + Component.onCompleted: resetText() Connections { target: MySettings - function onPromptTemplateChanged() { - templateTextArea.text = root.currentModelInfo.promptTemplate; - } + function onChatTemplateChanged() { templateTextArea.resetText(); } } Connections { target: root - function onCurrentModelInfoChanged() { - templateTextArea.text = root.currentModelInfo.promptTemplate; - } + function onCurrentModelIdChanged() { templateTextArea.resetText(); } + } + function legacyCheck() { + return /%[12]\b/.test(text) || !/\{%.*%\}.*\{\{.*\}\}.*\{%.*%\}/.test(text.replace(/\n/g, '')) + || !/\bcontent\b/.test(text); } onTextChanged: { - if (templateTextArea.text.indexOf("%1") !== -1) { - MySettings.setModelPromptTemplate(root.currentModelInfo, text) + const info = root.currentModelInfo; + let jinjaError; + if (!info.id) { + errMsg = null; + errState = "ok"; + } else if (info.chatTemplate.isLegacy && (isBeingReset || legacyCheck())) { + errMsg = null; + errState = "error"; + } else if (text === "" && !info.chatTemplate.isSet) { + errMsg = qsTr("No " + + "chat template configured."); + errState = "error"; + } else if (/^\s*$/.test(text)) { + errMsg = qsTr("The " + + "chat template cannot be blank."); + errState = "error"; + } else if ((jinjaError = MySettings.checkJinjaTemplateError(text)) !== null) { + errMsg = qsTr("Syntax" + + " error: %1").arg(jinjaError); + errState = "error"; + } else if (legacyCheck()) { + errMsg = qsTr("Chat template is not in " + + "" + + "Jinja format.") + errState = "warning"; + } else { + errState = "ok"; + } + if (info.id && errState !== "error" && !isBeingReset) + MySettings.setModelChatTemplate(info, text); + chatTemplateLabel.updateResetButton(); + } + Keys.onPressed: event => { + if (event.key === Qt.Key_Tab) { + const a = templateTextArea; + event.accepted = true; // suppress tab + a.insert(a.cursorPosition, ' '); // four spaces } } Accessible.role: Accessible.EditableText - Accessible.name: promptTemplateLabel.text - Accessible.description: promptTemplateLabelHelp.text + Accessible.name: chatTemplateLabel.text + Accessible.description: chatTemplateLabelHelp.text } } diff --git a/gpt4all-chat/qml/MySettingsButton.qml b/gpt4all-chat/qml/MySettingsButton.qml index 18de21afbbad..218a329c2f57 100644 --- a/gpt4all-chat/qml/MySettingsButton.qml +++ b/gpt4all-chat/qml/MySettingsButton.qml @@ -17,6 +17,7 @@ Button { property color borderColor: "transparent" property real fontPixelSize: theme.fontSizeLarge property string toolTip + property alias backgroundRadius: background.radius contentItem: Text { text: myButton.text @@ -28,6 +29,7 @@ Button { Accessible.name: text } background: Rectangle { + id: background radius: 10 border.width: borderWidth border.color: borderColor diff --git a/gpt4all-chat/qml/MySettingsLabel.qml b/gpt4all-chat/qml/MySettingsLabel.qml index 282bdc7332d3..2f0ba3c606b4 100644 --- a/gpt4all-chat/qml/MySettingsLabel.qml +++ b/gpt4all-chat/qml/MySettingsLabel.qml @@ -17,13 +17,42 @@ ColumnLayout { property alias color: mainTextLabel.color property alias linkColor: mainTextLabel.linkColor - Label { - id: mainTextLabel - color: theme.settingsTitleTextColor - font.pixelSize: theme.fontSizeLarger - font.bold: true - onLinkActivated: function(link) { - root.linkActivated(link); + property var onReset: null + property alias canReset: resetButton.enabled + property bool resetClears: false + + Item { + anchors.margins: 5 + width: childrenRect.width + height: mainTextLabel.contentHeight + + Label { + id: mainTextLabel + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + color: theme.settingsTitleTextColor + font.pixelSize: theme.fontSizeLarger + font.bold: true + verticalAlignment: Text.AlignVCenter + onLinkActivated: function(link) { + root.linkActivated(link); + } + } + + MySettingsButton { + id: resetButton + anchors.baseline: mainTextLabel.baseline + anchors.left: mainTextLabel.right + height: mainTextLabel.contentHeight + anchors.leftMargin: 10 + padding: 2 + leftPadding: 10 + rightPadding: 10 + backgroundRadius: 5 + text: resetClears ? qsTr("Clear") : qsTr("Reset") + visible: root.onReset !== null + onClicked: root.onReset() } } Label { diff --git a/gpt4all-chat/qml/MySettingsTab.qml b/gpt4all-chat/qml/MySettingsTab.qml index 98ed402ec666..41657f0b7c87 100644 --- a/gpt4all-chat/qml/MySettingsTab.qml +++ b/gpt4all-chat/qml/MySettingsTab.qml @@ -9,7 +9,7 @@ Item { property string title: "" property Item contentItem: null property bool showRestoreDefaultsButton: true - signal restoreDefaultsClicked + signal restoreDefaults onContentItemChanged: function() { if (contentItem) { @@ -19,6 +19,13 @@ Item { } } + ConfirmationDialog { + id: restoreDefaultsDialog + dialogTitle: qsTr("Restore defaults?") + description: qsTr("This page of settings will be reset to the defaults.") + onAccepted: root.restoreDefaults() + } + ScrollView { id: scrollView width: parent.width @@ -47,6 +54,7 @@ Item { Column { id: contentInner Layout.fillWidth: true + Layout.maximumWidth: parent.width } Item { @@ -63,9 +71,7 @@ Item { Accessible.role: Accessible.Button Accessible.name: text Accessible.description: qsTr("Restores settings dialog to a default state") - onClicked: { - root.restoreDefaultsClicked(); - } + onClicked: restoreDefaultsDialog.open() } } } diff --git a/gpt4all-chat/qml/MyTabButton.qml b/gpt4all-chat/qml/MyTabButton.qml new file mode 100644 index 000000000000..2a4609524741 --- /dev/null +++ b/gpt4all-chat/qml/MyTabButton.qml @@ -0,0 +1,26 @@ +import QtCore +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Basic +import mysettings +import mysettingsenums + +MySettingsButton { + property bool isSelected: false + contentItem: Text { + text: parent.text + horizontalAlignment: Qt.AlignCenter + color: isSelected ? theme.titleTextColor : theme.styledTextColor + font.pixelSize: theme.fontSizeLarger + } + background: Item { + visible: isSelected || hovered + Rectangle { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + height: 3 + color: isSelected ? theme.titleTextColor : theme.styledTextColorLighter + } + } +} diff --git a/gpt4all-chat/qml/MyTextArea.qml b/gpt4all-chat/qml/MyTextArea.qml index e0894e9fbb0f..bace1f26dc04 100644 --- a/gpt4all-chat/qml/MyTextArea.qml +++ b/gpt4all-chat/qml/MyTextArea.qml @@ -5,18 +5,27 @@ import QtQuick.Controls.Basic TextArea { id: myTextArea + + property string errState: "ok" // one of "ok", "error", "warning" + color: enabled ? theme.textColor : theme.mutedTextColor placeholderTextColor: theme.mutedTextColor font.pixelSize: theme.fontSizeLarge background: Rectangle { implicitWidth: 150 color: theme.controlBackground - border.width: 1 - border.color: theme.controlBorder + border.width: errState === "ok" ? 1 : 2 + border.color: { + switch (errState) { + case "ok": return theme.controlBorder; + case "warning": return theme.textWarningColor; + case "error": return theme.textErrorColor; + } + } radius: 10 } padding: 10 wrapMode: TextArea.Wrap ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval -} \ No newline at end of file +} diff --git a/gpt4all-chat/qml/MyToolButton.qml b/gpt4all-chat/qml/MyToolButton.qml index b9d8c544aa1e..046c46851274 100644 --- a/gpt4all-chat/qml/MyToolButton.qml +++ b/gpt4all-chat/qml/MyToolButton.qml @@ -16,6 +16,7 @@ Button { property alias fillMode: image.fillMode property alias imageWidth: image.sourceSize.width property alias imageHeight: image.sourceSize.height + property alias bgTransform: background.transform contentItem: Text { text: myButton.text horizontalAlignment: Text.AlignHCenter @@ -26,6 +27,7 @@ Button { } background: Item { + id: background anchors.fill: parent Rectangle { anchors.fill: parent @@ -47,7 +49,7 @@ Button { ColorOverlay { anchors.fill: image source: image - color: myButton.hovered ? backgroundColorHovered : backgroundColor + color: !myButton.enabled ? theme.mutedTextColor : myButton.hovered ? backgroundColorHovered : backgroundColor } } Accessible.role: Accessible.Button diff --git a/gpt4all-chat/qml/StartupDialog.qml b/gpt4all-chat/qml/StartupDialog.qml index a1d021c89ae6..a29a8a4ce280 100644 --- a/gpt4all-chat/qml/StartupDialog.qml +++ b/gpt4all-chat/qml/StartupDialog.qml @@ -115,7 +115,7 @@ model release that uses your data!") anchors.right: parent.right Label { id: optInStatistics - text: "Opt-in to anonymous usage analytics used to improve GPT4All" + text: qsTr("Opt-in to anonymous usage analytics used to improve GPT4All") Layout.row: 0 Layout.column: 0 color: theme.textColor @@ -229,7 +229,7 @@ model release that uses your data!") Label { id: optInNetwork - text: "Opt-in to anonymous sharing of chats to the GPT4All Datalake" + text: qsTr("Opt-in to anonymous sharing of chats to the GPT4All Datalake") Layout.row: 1 Layout.column: 0 color: theme.textColor diff --git a/gpt4all-chat/qml/SwitchModelDialog.qml b/gpt4all-chat/qml/SwitchModelDialog.qml deleted file mode 100644 index f0ca43abbc24..000000000000 --- a/gpt4all-chat/qml/SwitchModelDialog.qml +++ /dev/null @@ -1,46 +0,0 @@ -import QtCore -import QtQuick -import QtQuick.Controls -import QtQuick.Controls.Basic -import QtQuick.Layouts -import llm -import mysettings - -MyDialog { - id: switchModelDialog - anchors.centerIn: parent - modal: true - padding: 20 - property int index: -1 - - Theme { - id: theme - } - - contentItem: Text { - textFormat: Text.StyledText - text: qsTr("Warning: changing the model will erase the current conversation. Do you wish to continue?") - color: theme.textColor - font.pixelSize: theme.fontSizeLarge - } - - footer: DialogButtonBox { - id: dialogBox - padding: 20 - alignment: Qt.AlignRight - spacing: 10 - MySettingsButton { - text: qsTr("Continue") - Accessible.description: qsTr("Continue with model loading") - DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole - } - MySettingsButton { - text: qsTr("Cancel") - Accessible.description: qsTr("Cancel") - DialogButtonBox.buttonRole: DialogButtonBox.RejectRole - } - background: Rectangle { - color: "transparent" - } - } -} diff --git a/gpt4all-chat/qml/Theme.qml b/gpt4all-chat/qml/Theme.qml index 245a4473b0e8..4a33bd8c168c 100644 --- a/gpt4all-chat/qml/Theme.qml +++ b/gpt4all-chat/qml/Theme.qml @@ -64,6 +64,9 @@ QtObject { property color green800: Qt.hsla(123/360, 0.17, 0.24) property color green900: Qt.hsla(124/360, 0.17, 0.20) property color green950: Qt.hsla(125/360, 0.22, 0.10) + property color green300_sat: Qt.hsla(122/360, 0.24, 0.73) + property color green400_sat: Qt.hsla(122/360, 0.23, 0.58) + property color green450_sat: Qt.hsla(122/360, 0.23, 0.52) // yellow property color yellow0: Qt.hsla(47/360, 0.90, 0.99) @@ -99,6 +102,7 @@ QtObject { property color purple200: Qt.hsla(279/360, 1.0, 0.91) property color purple300: Qt.hsla(279/360, 1.0, 0.84) property color purple400: Qt.hsla(279/360, 1.0, 0.73) + property color purple450: Qt.hsla(279/360, 1.0, 0.68) property color purple500: Qt.hsla(279/360, 1.0, 0.63) property color purple600: Qt.hsla(279/360, 1.0, 0.53) property color purple700: Qt.hsla(279/360, 1.0, 0.47) @@ -408,6 +412,39 @@ QtObject { } } + property color mediumButtonBackground: { + switch (MySettings.chatTheme) { + case MySettingsEnums.ChatTheme.LegacyDark: + return purple400 + case MySettingsEnums.ChatTheme.Dark: + return green400_sat + default: + return green400_sat + } + } + + property color mediumButtonBackgroundHovered: { + switch (MySettings.chatTheme) { + case MySettingsEnums.ChatTheme.LegacyDark: + return purple450 + case MySettingsEnums.ChatTheme.Dark: + return green450_sat + default: + return green300_sat + } + } + + property color mediumButtonText: { + switch (MySettings.chatTheme) { + case MySettingsEnums.ChatTheme.LegacyDark: + return textColor + case MySettingsEnums.ChatTheme.Dark: + return textColor + default: + return white + } + } + property color darkButtonText: { switch (MySettings.chatTheme) { case MySettingsEnums.ChatTheme.LegacyDark: @@ -922,16 +959,8 @@ QtObject { } } - property color textErrorColor: { - switch (MySettings.chatTheme) { - case MySettingsEnums.ChatTheme.LegacyDark: - return red400 - case MySettingsEnums.ChatTheme.Dark: - return red400 - default: - return red400 - } - } + readonly property color textErrorColor: red400 + readonly property color textWarningColor: yellow400 property color settingsTitleTextColor: { switch (MySettings.chatTheme) { @@ -988,6 +1017,17 @@ QtObject { } } + property color styledTextColorLighter: { + switch (MySettings.chatTheme) { + case MySettingsEnums.ChatTheme.LegacyDark: + return purple50 + case MySettingsEnums.ChatTheme.Dark: + return yellow0 + default: + return grayRed400 + } + } + property color styledTextColor2: { switch (MySettings.chatTheme) { case MySettingsEnums.ChatTheme.LegacyDark: @@ -1227,5 +1267,6 @@ QtObject { property real fontSizeLarger: 14 * fontScale property real fontSizeLargest: 18 * fontScale property real fontSizeBannerSmall: 24 * fontScale**.8 - property real fontSizeBanner: 48 * fontScale**.8 + property real fontSizeBanner: 32 * fontScale**.8 + property real fontSizeBannerLarge: 48 * fontScale**.8 } diff --git a/gpt4all-chat/src/chat.cpp b/gpt4all-chat/src/chat.cpp index dc6abd0621d9..725d2374cb79 100644 --- a/gpt4all-chat/src/chat.cpp +++ b/gpt4all-chat/src/chat.cpp @@ -1,23 +1,30 @@ #include "chat.h" #include "chatlistmodel.h" -#include "mysettings.h" #include "network.h" #include "server.h" +#include "tool.h" +#include "toolcallparser.h" +#include "toolmodel.h" #include #include #include +#include +#include +#include #include #include +#include #include -#include #include #include #include #include +using namespace ToolEnums; + Chat::Chat(QObject *parent) : QObject(parent) , m_id(Network::globalInstance()->generateUniqueId()) @@ -61,13 +68,12 @@ void Chat::connectLLM() connect(m_llmodel, &ChatLLM::responseStopped, this, &Chat::responseStopped, Qt::QueuedConnection); connect(m_llmodel, &ChatLLM::modelLoadingError, this, &Chat::handleModelLoadingError, Qt::QueuedConnection); connect(m_llmodel, &ChatLLM::modelLoadingWarning, this, &Chat::modelLoadingWarning, Qt::QueuedConnection); - connect(m_llmodel, &ChatLLM::restoringFromTextChanged, this, &Chat::handleRestoringFromText, Qt::QueuedConnection); connect(m_llmodel, &ChatLLM::generatedNameChanged, this, &Chat::generatedNameChanged, Qt::QueuedConnection); connect(m_llmodel, &ChatLLM::generatedQuestionFinished, this, &Chat::generatedQuestionFinished, Qt::QueuedConnection); connect(m_llmodel, &ChatLLM::reportSpeed, this, &Chat::handleTokenSpeedChanged, Qt::QueuedConnection); connect(m_llmodel, &ChatLLM::loadedModelInfoChanged, this, &Chat::loadedModelInfoChanged, Qt::QueuedConnection); connect(m_llmodel, &ChatLLM::databaseResultsChanged, this, &Chat::handleDatabaseResultsChanged, Qt::QueuedConnection); - connect(m_llmodel, &ChatLLM::modelInfoChanged, this, &Chat::handleModelInfoChanged, Qt::QueuedConnection); + connect(m_llmodel, &ChatLLM::modelInfoChanged, this, &Chat::handleModelChanged, Qt::QueuedConnection); connect(m_llmodel, &ChatLLM::trySwitchContextOfLoadedModelCompleted, this, &Chat::handleTrySwitchContextOfLoadedModelCompleted, Qt::QueuedConnection); connect(this, &Chat::promptRequested, m_llmodel, &ChatLLM::prompt, Qt::QueuedConnection); @@ -75,11 +81,10 @@ void Chat::connectLLM() connect(this, &Chat::loadDefaultModelRequested, m_llmodel, &ChatLLM::loadDefaultModel, Qt::QueuedConnection); connect(this, &Chat::generateNameRequested, m_llmodel, &ChatLLM::generateName, Qt::QueuedConnection); connect(this, &Chat::regenerateResponseRequested, m_llmodel, &ChatLLM::regenerateResponse, Qt::QueuedConnection); - connect(this, &Chat::resetResponseRequested, m_llmodel, &ChatLLM::resetResponse, Qt::QueuedConnection); - connect(this, &Chat::resetContextRequested, m_llmodel, &ChatLLM::resetContext, Qt::QueuedConnection); - connect(this, &Chat::processSystemPromptRequested, m_llmodel, &ChatLLM::processSystemPrompt, Qt::QueuedConnection); connect(this, &Chat::collectionListChanged, m_collectionModel, &LocalDocsCollectionsModel::setCollections); + + connect(ModelList::globalInstance(), &ModelList::modelInfoChanged, this, &Chat::handleModelInfoChanged); } void Chat::reset() @@ -87,28 +92,17 @@ void Chat::reset() stopGenerating(); // Erase our current on disk representation as we're completely resetting the chat along with id ChatListModel::globalInstance()->removeChatFile(this); - emit resetContextRequested(); m_id = Network::globalInstance()->generateUniqueId(); emit idChanged(m_id); // NOTE: We deliberately do no reset the name or creation date to indicate that this was originally // an older chat that was reset for another purpose. Resetting this data will lead to the chat // name label changing back to 'New Chat' and showing up in the chat model list as a 'New Chat' // further down in the list. This might surprise the user. In the future, we might get rid of - // the "reset context" button in the UI. Right now, by changing the model in the combobox dropdown - // we effectively do a reset context. We *have* to do this right now when switching between different - // types of models. The only way to get rid of that would be a very long recalculate where we rebuild - // the context if we switch between different types of models. Probably the right way to fix this - // is to allow switching models but throwing up a dialog warning users if we switch between types - // of models that a long recalculation will ensue. + // the "reset context" button in the UI. m_chatModel->clear(); m_needsSave = true; } -void Chat::processSystemPrompt() -{ - emit processSystemPromptRequested(); -} - void Chat::resetResponseState() { if (m_responseInProgress && m_responseState == Chat::LocalDocsRetrieval) @@ -160,29 +154,38 @@ void Chat::newPromptResponsePair(const QString &prompt, const QList &attac if (!attachedContexts.isEmpty()) promptPlusAttached = attachedContexts.join("\n\n") + "\n\n" + prompt; - newPromptResponsePairInternal(prompt, attachments); - emit resetResponseRequested(); + resetResponseState(); + if (int count = m_chatModel->count()) + m_chatModel->updateCurrentResponse(count - 1, false); + m_chatModel->appendPrompt(prompt, attachments); + m_chatModel->appendResponse(); - this->prompt(promptPlusAttached); + emit promptRequested(m_collections); + m_needsSave = true; } -void Chat::prompt(const QString &prompt) +void Chat::regenerateResponse(int index) { resetResponseState(); - emit promptRequested(m_collections, prompt); + emit regenerateResponseRequested(index); m_needsSave = true; } -void Chat::regenerateResponse() +QVariant Chat::popPrompt(int index) { - const int index = m_chatModel->count() - 1; - m_chatModel->updateSources(index, QList()); - emit regenerateResponseRequested(); + auto content = m_llmodel->popPrompt(index); m_needsSave = true; + if (content) return *content; + return QVariant::fromValue(nullptr); } void Chat::stopGenerating() { + // In future if we have more than one tool we'll have to keep track of which tools are possibly + // running, but for now we only have one + Tool *toolInstance = ToolModel::globalInstance()->get(ToolCallConstants::CodeInterpreterFunction); + Q_ASSERT(toolInstance); + toolInstance->interrupt(); m_llmodel->stopGenerating(); } @@ -191,15 +194,12 @@ Chat::ResponseState Chat::responseState() const return m_responseState; } -void Chat::handleResponseChanged(const QString &response) +void Chat::handleResponseChanged() { if (m_responseState != Chat::ResponseGeneration) { m_responseState = Chat::ResponseGeneration; emit responseStateChanged(); } - - const int index = m_chatModel->count() - 1; - m_chatModel->updateValue(index, response); } void Chat::handleModelLoadingPercentageChanged(float loadingPercentage) @@ -244,14 +244,74 @@ void Chat::responseStopped(qint64 promptResponseMs) m_responseState = Chat::ResponseStopped; emit responseInProgressChanged(); emit responseStateChanged(); - if (m_generatedName.isEmpty()) - emit generateNameRequested(); - Network::globalInstance()->trackChatEvent("response_complete", { + const QString possibleToolcall = m_chatModel->possibleToolcall(); + + Network::globalInstance()->trackChatEvent("response_stopped", { {"first", m_firstResponse}, {"message_count", chatModel()->count()}, {"$duration", promptResponseMs / 1000.}, }); + + ToolCallParser parser; + parser.update(possibleToolcall.toUtf8()); + if (parser.state() == ToolEnums::ParseState::Complete && parser.startTag() != ToolCallConstants::ThinkTag) + processToolCall(parser.toolCall()); + else + responseComplete(); +} + +void Chat::processToolCall(const QString &toolCall) +{ + m_responseState = Chat::ToolCallGeneration; + emit responseStateChanged(); + // Regex to remove the formatting around the code + static const QRegularExpression regex("^\\s*```javascript\\s*|\\s*```\\s*$"); + QString code = toolCall; + code.remove(regex); + code = code.trimmed(); + + // Right now the code interpreter is the only available tool + Tool *toolInstance = ToolModel::globalInstance()->get(ToolCallConstants::CodeInterpreterFunction); + Q_ASSERT(toolInstance); + connect(toolInstance, &Tool::runComplete, this, &Chat::toolCallComplete, Qt::SingleShotConnection); + + // The param is the code + const ToolParam param = { "code", ToolEnums::ParamType::String, code }; + m_responseInProgress = true; + emit responseInProgressChanged(); + toolInstance->run({param}); +} + +void Chat::toolCallComplete(const ToolCallInfo &info) +{ + // Update the current response with meta information about toolcall and re-parent + m_chatModel->updateToolCall(info); + + ++m_consecutiveToolCalls; + + m_responseInProgress = false; + emit responseInProgressChanged(); + + // We limit the number of consecutive toolcalls otherwise we get into a potentially endless loop + if (m_consecutiveToolCalls < 3 || info.error == ToolEnums::Error::NoError) { + resetResponseState(); + emit promptRequested(m_collections); // triggers a new response + return; + } + + responseComplete(); +} + +void Chat::responseComplete() +{ + if (m_generatedName.isEmpty()) + emit generateNameRequested(); + + m_responseState = Chat::ResponseStopped; + emit responseStateChanged(); + + m_consecutiveToolCalls = 0; m_firstResponse = false; } @@ -272,25 +332,6 @@ void Chat::setModelInfo(const ModelInfo &modelInfo) emit modelChangeRequested(modelInfo); } -// the server needs to block until response is reset, so it calls resetResponse on its own m_llmThread -void Chat::serverNewPromptResponsePair(const QString &prompt, const QList &attachments) -{ - newPromptResponsePairInternal(prompt, attachments); -} - -void Chat::newPromptResponsePairInternal(const QString &prompt, const QList &attachments) -{ - resetResponseState(); - m_chatModel->updateCurrentResponse(m_chatModel->count() - 1, false); - m_chatModel->appendPrompt("Prompt: ", prompt, attachments); - m_chatModel->appendResponse("Response: "); -} - -bool Chat::restoringFromText() const -{ - return m_llmodel->restoringFromText(); -} - void Chat::unloadAndDeleteLater() { if (!isModelLoaded()) { @@ -356,12 +397,6 @@ void Chat::generatedQuestionFinished(const QString &question) m_needsSave = true; } -void Chat::handleRestoringFromText() -{ - Network::globalInstance()->trackChatEvent("recalc_context", { {"length", m_chatModel->count()} }); - emit restoringFromTextChanged(); -} - void Chat::handleModelLoadingError(const QString &error) { if (!error.isEmpty()) { @@ -396,12 +431,19 @@ QString Chat::fallbackReason() const void Chat::handleDatabaseResultsChanged(const QList &results) { m_databaseResults = results; - const int index = m_chatModel->count() - 1; - m_chatModel->updateSources(index, m_databaseResults); m_needsSave = true; } +// we need to notify listeners of the modelInfo property when its properties are updated, +// since it's a gadget and can't do that on its own void Chat::handleModelInfoChanged(const ModelInfo &modelInfo) +{ + if (!m_modelInfo.id().isNull() && modelInfo.id() == m_modelInfo.id()) + emit modelInfoChanged(); +} + +// react if a new model is loaded +void Chat::handleModelChanged(const ModelInfo &modelInfo) { if (m_modelInfo == modelInfo) return; @@ -430,10 +472,7 @@ bool Chat::serialize(QDataStream &stream, int version) const if (version >= 3) stream << m_collections; - const bool serializeKV = MySettings::globalInstance()->saveChatsContext(); - if (version >= 6) - stream << serializeKV; - if (!m_llmodel->serialize(stream, version, serializeKV)) + if (!m_llmodel->serialize(stream, version)) return false; if (!m_chatModel->serialize(stream, version)) return false; @@ -462,19 +501,13 @@ bool Chat::deserialize(QDataStream &stream, int version) if (!m_modelInfo.id().isEmpty()) emit modelInfoChanged(); - bool discardKV = m_modelInfo.id().isEmpty(); - if (version >= 3) { stream >> m_collections; emit collectionListChanged(m_collections); } - bool deserializeKV = true; - if (version >= 6) - stream >> deserializeKV; - m_llmodel->setModelInfo(m_modelInfo); - if (!m_llmodel->deserialize(stream, version, deserializeKV, discardKV)) + if (!m_llmodel->deserialize(stream, version)) return false; if (!m_chatModel->deserialize(stream, version)) return false; diff --git a/gpt4all-chat/src/chat.h b/gpt4all-chat/src/chat.h index 245bc018d40c..7ac2c65d598c 100644 --- a/gpt4all-chat/src/chat.h +++ b/gpt4all-chat/src/chat.h @@ -12,6 +12,8 @@ #include #include #include +#include // IWYU pragma: keep +#include #include class QDataStream; @@ -27,7 +29,6 @@ class Chat : public QObject Q_PROPERTY(float modelLoadingPercentage READ modelLoadingPercentage NOTIFY modelLoadingPercentageChanged) Q_PROPERTY(ModelInfo modelInfo READ modelInfo WRITE setModelInfo NOTIFY modelInfoChanged) Q_PROPERTY(bool responseInProgress READ responseInProgress NOTIFY responseInProgressChanged) - Q_PROPERTY(bool restoringFromText READ restoringFromText NOTIFY restoringFromTextChanged) Q_PROPERTY(bool isServer READ isServer NOTIFY isServerChanged) Q_PROPERTY(ResponseState responseState READ responseState NOTIFY responseStateChanged) Q_PROPERTY(QList collectionList READ collectionList NOTIFY collectionListChanged) @@ -54,7 +55,8 @@ class Chat : public QObject LocalDocsProcessing, PromptProcessing, GeneratingQuestions, - ResponseGeneration + ResponseGeneration, + ToolCallGeneration }; Q_ENUM(ResponseState) @@ -77,13 +79,12 @@ class Chat : public QObject bool isNewChat() const { return m_name == tr("New Chat") && !m_chatModel->count(); } Q_INVOKABLE void reset(); - Q_INVOKABLE void processSystemPrompt(); bool isModelLoaded() const { return m_modelLoadingPercentage == 1.0f; } bool isCurrentlyLoading() const { return m_modelLoadingPercentage > 0.0f && m_modelLoadingPercentage < 1.0f; } float modelLoadingPercentage() const { return m_modelLoadingPercentage; } Q_INVOKABLE void newPromptResponsePair(const QString &prompt, const QList &attachedUrls = {}); - Q_INVOKABLE void prompt(const QString &prompt); - Q_INVOKABLE void regenerateResponse(); + Q_INVOKABLE void regenerateResponse(int index); + Q_INVOKABLE QVariant popPrompt(int index); Q_INVOKABLE void stopGenerating(); QList databaseResults() const { return m_databaseResults; } @@ -92,7 +93,6 @@ class Chat : public QObject ResponseState responseState() const; ModelInfo modelInfo() const; void setModelInfo(const ModelInfo &modelInfo); - bool restoringFromText() const; Q_INVOKABLE void unloadModel(); Q_INVOKABLE void reloadModel(); @@ -113,7 +113,6 @@ class Chat : public QObject Q_INVOKABLE bool hasCollection(const QString &collection) const; Q_INVOKABLE void addCollection(const QString &collection); Q_INVOKABLE void removeCollection(const QString &collection); - void resetResponseState(); QString modelLoadingError() const { return m_modelLoadingError; } @@ -131,7 +130,7 @@ class Chat : public QObject void setNeedsSave(bool n) { m_needsSave = n; } public Q_SLOTS: - void serverNewPromptResponsePair(const QString &prompt, const QList &attachments = {}); + void resetResponseState(); Q_SIGNALS: void idChanged(const QString &id); @@ -143,14 +142,12 @@ public Q_SLOTS: void modelLoadingWarning(const QString &warning); void responseInProgressChanged(); void responseStateChanged(); - void promptRequested(const QList &collectionList, const QString &prompt); - void regenerateResponseRequested(); + void promptRequested(const QStringList &enabledCollections); + void regenerateResponseRequested(int index); void resetResponseRequested(); void resetContextRequested(); - void processSystemPromptRequested(); void modelChangeRequested(const ModelInfo &modelInfo); void modelInfoChanged(); - void restoringFromTextChanged(); void loadDefaultModelRequested(); void generateNameRequested(); void modelLoadingErrorChanged(); @@ -165,23 +162,23 @@ public Q_SLOTS: void generatedQuestionsChanged(); private Q_SLOTS: - void handleResponseChanged(const QString &response); + void handleResponseChanged(); void handleModelLoadingPercentageChanged(float); void promptProcessing(); void generatingQuestions(); void responseStopped(qint64 promptResponseMs); + void processToolCall(const QString &toolCall); + void toolCallComplete(const ToolCallInfo &info); + void responseComplete(); void generatedNameChanged(const QString &name); void generatedQuestionFinished(const QString &question); - void handleRestoringFromText(); void handleModelLoadingError(const QString &error); void handleTokenSpeedChanged(const QString &tokenSpeed); void handleDatabaseResultsChanged(const QList &results); void handleModelInfoChanged(const ModelInfo &modelInfo); + void handleModelChanged(const ModelInfo &modelInfo); void handleTrySwitchContextOfLoadedModelCompleted(int value); -private: - void newPromptResponsePairInternal(const QString &prompt, const QList &attachments); - private: QString m_id; QString m_name; @@ -211,6 +208,7 @@ private Q_SLOTS: // - The chat was freshly created during this launch. // - The chat was changed after loading it from disk. bool m_needsSave = true; + int m_consecutiveToolCalls = 0; }; #endif // CHAT_H diff --git a/gpt4all-chat/src/chatapi.cpp b/gpt4all-chat/src/chatapi.cpp index 27f64f0d6730..5164cac32169 100644 --- a/gpt4all-chat/src/chatapi.cpp +++ b/gpt4all-chat/src/chatapi.cpp @@ -1,10 +1,10 @@ #include "chatapi.h" -#include +#include "utils.h" #include -#include #include +#include #include #include #include @@ -13,12 +13,17 @@ #include #include #include +#include #include +#include #include #include #include +#include +#include #include +#include using namespace Qt::Literals::StringLiterals; @@ -67,71 +72,119 @@ bool ChatAPI::isModelLoaded() const return true; } -void ChatAPI::prompt(const std::string &prompt, - const std::string &promptTemplate, - std::function promptCallback, - std::function responseCallback, - bool allowContextShift, - PromptContext &promptCtx, - bool special, - std::optional fakeReply) { - - Q_UNUSED(promptCallback); - Q_UNUSED(allowContextShift); - Q_UNUSED(special); - - if (!isModelLoaded()) { - std::cerr << "ChatAPI ERROR: prompt won't work with an unloaded model!\n"; - return; - } - - if (!promptCtx.n_past) { m_queuedPrompts.clear(); } - Q_ASSERT(promptCtx.n_past <= m_context.size()); - m_context.resize(promptCtx.n_past); - - // FIXME(cebtenzzre): We're assuming people don't try to use %2 with ChatGPT. What would that even mean? - m_queuedPrompts << QString::fromStdString(promptTemplate).arg(QString::fromStdString(prompt)); +static auto parsePrompt(QXmlStreamReader &xml) -> std::expected +{ + QJsonArray messages; - if (!promptCtx.n_predict && !fakeReply) { - return; // response explicitly suppressed, queue prompt for later + auto xmlError = [&xml] { + return std::unexpected(u"%1:%2: %3"_s.arg(xml.lineNumber()).arg(xml.columnNumber()).arg(xml.errorString())); + }; + + if (xml.hasError()) + return xmlError(); + if (xml.atEnd()) + return messages; + + // skip header + bool foundElement = false; + do { + switch (xml.readNext()) { + using enum QXmlStreamReader::TokenType; + case Invalid: + return xmlError(); + case EndDocument: + return messages; + default: + foundElement = true; + case StartDocument: + case Comment: + case DTD: + case ProcessingInstruction: + ; + } + } while (!foundElement); + + // document body loop + bool foundRoot = false; + for (;;) { + switch (xml.tokenType()) { + using enum QXmlStreamReader::TokenType; + case StartElement: + { + auto name = xml.name(); + if (!foundRoot) { + if (name != "chat"_L1) + return std::unexpected(u"unexpected tag: %1"_s.arg(name)); + foundRoot = true; + } else { + if (name != "user"_L1 && name != "assistant"_L1 && name != "system"_L1) + return std::unexpected(u"unknown role: %1"_s.arg(name)); + auto content = xml.readElementText(); + if (xml.tokenType() != EndElement) + return xmlError(); + messages << makeJsonObject({ + { "role"_L1, name.toString().trimmed() }, + { "content"_L1, content }, + }); + } + break; + } + case Characters: + if (!xml.isWhitespace()) + return std::unexpected(u"unexpected text: %1"_s.arg(xml.text())); + case Comment: + case ProcessingInstruction: + case EndElement: + break; + case EndDocument: + return messages; + case Invalid: + return xmlError(); + default: + return std::unexpected(u"unexpected token: %1"_s.arg(xml.tokenString())); + } + xml.readNext(); } +} - QString formattedPrompt = m_queuedPrompts.join(""); - m_queuedPrompts.clear(); +void ChatAPI::prompt( + std::string_view prompt, + const PromptCallback &promptCallback, + const ResponseCallback &responseCallback, + const PromptContext &promptCtx +) { + Q_UNUSED(promptCallback) - if (fakeReply) { - promptCtx.n_past += 1; - m_context.append(formattedPrompt); - m_context.append(QString::fromUtf8(fakeReply->data(), fakeReply->size())); - return; - } + if (!isModelLoaded()) + throw std::invalid_argument("Attempted to prompt an unloaded model."); + if (!promptCtx.n_predict) + return; // nothing requested // FIXME: We don't set the max_tokens on purpose because in order to do so safely without encountering // an error we need to be able to count the tokens in our prompt. The only way to do this is to use - // the OpenAI tiktokken library or to implement our own tokenization function that matches precisely + // the OpenAI tiktoken library or to implement our own tokenization function that matches precisely // the tokenization used by the OpenAI model we're calling. OpenAI has not introduced any means of // using the REST API to count tokens in a prompt. - QJsonObject root; - root.insert("model", m_modelName); - root.insert("stream", true); - root.insert("temperature", promptCtx.temp); - root.insert("top_p", promptCtx.top_p); + auto root = makeJsonObject({ + { "model"_L1, m_modelName }, + { "stream"_L1, true }, + { "temperature"_L1, promptCtx.temp }, + { "top_p"_L1, promptCtx.top_p }, + }); // conversation history - QJsonArray messages; - for (int i = 0; i < m_context.count(); ++i) { - QJsonObject message; - message.insert("role", i % 2 == 0 ? "user" : "assistant"); - message.insert("content", m_context.at(i)); - messages.append(message); + { + QUtf8StringView promptUtf8(prompt); + QXmlStreamReader xml(promptUtf8); + auto messages = parsePrompt(xml); + if (!messages) { + auto error = fmt::format("Failed to parse API model prompt: {}", messages.error()); + qDebug().noquote() << "ChatAPI ERROR:" << error << "Prompt:\n\n" << promptUtf8 << '\n'; + throw std::invalid_argument(error); + } + root.insert("messages"_L1, *messages); } - QJsonObject promptObject; - promptObject.insert("role", "user"); - promptObject.insert("content", formattedPrompt); - messages.append(promptObject); - root.insert("messages", messages); - QJsonDocument doc(root); #if defined(DEBUG) @@ -148,12 +201,9 @@ void ChatAPI::prompt(const std::string &prompt, connect(&worker, &ChatAPIWorker::finished, &workerThread, &QThread::quit, Qt::DirectConnection); connect(this, &ChatAPI::request, &worker, &ChatAPIWorker::request, Qt::QueuedConnection); workerThread.start(); - emit request(m_apiKey, &promptCtx, doc.toJson(QJsonDocument::Compact)); + emit request(m_apiKey, doc.toJson(QJsonDocument::Compact)); workerThread.wait(); - promptCtx.n_past += 1; - m_context.append(formattedPrompt); - m_context.append(worker.currentResponse()); m_responseCallback = nullptr; #if defined(DEBUG) @@ -171,12 +221,8 @@ bool ChatAPI::callResponse(int32_t token, const std::string& string) return m_responseCallback(token, string); } -void ChatAPIWorker::request(const QString &apiKey, - LLModel::PromptContext *promptCtx, - const QByteArray &array) +void ChatAPIWorker::request(const QString &apiKey, const QByteArray &array) { - m_ctx = promptCtx; - QUrl apiUrl(m_chat->url()); const QString authorization = u"Bearer %1"_s.arg(apiKey).trimmed(); QNetworkRequest request(apiUrl); @@ -283,7 +329,6 @@ void ChatAPIWorker::handleReadyRead() const QJsonObject choice = choices.first().toObject(); const QJsonObject delta = choice.value("delta").toObject(); const QString content = delta.value("content").toString(); - Q_ASSERT(m_ctx); m_currentResponse += content; if (!m_chat->callResponse(0, content.toStdString())) { reply->abort(); diff --git a/gpt4all-chat/src/chatapi.h b/gpt4all-chat/src/chatapi.h index f37a105d29f1..b763c32524b2 100644 --- a/gpt4all-chat/src/chatapi.h +++ b/gpt4all-chat/src/chatapi.h @@ -7,17 +7,14 @@ #include #include #include -#include -#include #include #include -#include -#include #include #include #include #include +#include #include class QNetworkAccessManager; @@ -28,16 +25,13 @@ class ChatAPIWorker : public QObject { public: ChatAPIWorker(ChatAPI *chatAPI) : QObject(nullptr) - , m_ctx(nullptr) , m_networkManager(nullptr) , m_chat(chatAPI) {} virtual ~ChatAPIWorker() {} QString currentResponse() const { return m_currentResponse; } - void request(const QString &apiKey, - LLModel::PromptContext *promptCtx, - const QByteArray &array); + void request(const QString &apiKey, const QByteArray &array); Q_SIGNALS: void finished(); @@ -49,7 +43,6 @@ private Q_SLOTS: private: ChatAPI *m_chat; - LLModel::PromptContext *m_ctx; QNetworkAccessManager *m_networkManager; QString m_currentResponse; }; @@ -74,14 +67,14 @@ class ChatAPI : public QObject, public LLModel { size_t restoreState(std::span state, std::span inputTokens) override { Q_UNUSED(state); Q_UNUSED(inputTokens); throwNotImplemented(); } - void prompt(const std::string &prompt, - const std::string &promptTemplate, - std::function promptCallback, - std::function responseCallback, - bool allowContextShift, - PromptContext &ctx, - bool special, - std::optional fakeReply) override; + void prompt(std::string_view prompt, + const PromptCallback &promptCallback, + const ResponseCallback &responseCallback, + const PromptContext &ctx) override; + + [[noreturn]] + int32_t countPromptTokens(std::string_view prompt) const override + { Q_UNUSED(prompt); throwNotImplemented(); } void setThreadCount(int32_t n_threads) override; int32_t threadCount() const override; @@ -91,19 +84,17 @@ class ChatAPI : public QObject, public LLModel { void setRequestURL(const QString &requestURL) { m_requestURL = requestURL; } QString url() const { return m_requestURL; } - QList context() const { return m_context; } - void setContext(const QList &context) { m_context = context; } - bool callResponse(int32_t token, const std::string &string); [[noreturn]] int32_t contextLength() const override { throwNotImplemented(); } + auto specialTokens() -> std::unordered_map const override + { return {}; } + Q_SIGNALS: - void request(const QString &apiKey, - LLModel::PromptContext *ctx, - const QByteArray &array); + void request(const QString &apiKey, const QByteArray &array); protected: // We have to implement these as they are pure virtual in base class, but we don't actually use @@ -114,8 +105,8 @@ class ChatAPI : public QObject, public LLModel { static void throwNotImplemented() { throw std::logic_error("not implemented"); } [[noreturn]] - std::vector tokenize(std::string_view str, bool special) override - { Q_UNUSED(str); Q_UNUSED(special); throwNotImplemented(); } + std::vector tokenize(std::string_view str) const override + { Q_UNUSED(str); throwNotImplemented(); } [[noreturn]] bool isSpecialToken(Token id) const override @@ -126,7 +117,7 @@ class ChatAPI : public QObject, public LLModel { { Q_UNUSED(id); throwNotImplemented(); } [[noreturn]] - void initSampler(PromptContext &ctx) override + void initSampler(const PromptContext &ctx) override { Q_UNUSED(ctx); throwNotImplemented(); } [[noreturn]] @@ -134,33 +125,28 @@ class ChatAPI : public QObject, public LLModel { { throwNotImplemented(); } [[noreturn]] - bool evalTokens(PromptContext &ctx, std::span tokens) const override - { Q_UNUSED(ctx); Q_UNUSED(tokens); throwNotImplemented(); } + bool evalTokens(int32_t nPast, std::span tokens) const override + { Q_UNUSED(nPast); Q_UNUSED(tokens); throwNotImplemented(); } [[noreturn]] - void shiftContext(PromptContext &promptCtx) override - { Q_UNUSED(promptCtx); throwNotImplemented(); } + void shiftContext(const PromptContext &promptCtx, int32_t *nPast) override + { Q_UNUSED(promptCtx); Q_UNUSED(nPast); throwNotImplemented(); } [[noreturn]] int32_t inputLength() const override { throwNotImplemented(); } [[noreturn]] - void setTokenizeInputPosition(int32_t pos) override - { Q_UNUSED(pos); throwNotImplemented(); } - - [[noreturn]] - auto computeModelInputPosition(PromptContext &ctx, const std::vector &input) - -> std::vector::const_iterator override - { Q_UNUSED(ctx); Q_UNUSED(input); throwNotImplemented(); } + int32_t computeModelInputPosition(std::span input) const override + { Q_UNUSED(input); throwNotImplemented(); } [[noreturn]] - void setModelInputPosition(PromptContext &ctx, int32_t pos) override - { Q_UNUSED(ctx); Q_UNUSED(pos); throwNotImplemented(); } + void setModelInputPosition(int32_t pos) override + { Q_UNUSED(pos); throwNotImplemented(); } [[noreturn]] - void appendInputToken(PromptContext &ctx, Token tok) override - { Q_UNUSED(ctx); Q_UNUSED(tok); throwNotImplemented(); } + void appendInputToken(Token tok) override + { Q_UNUSED(tok); throwNotImplemented(); } [[noreturn]] const std::vector &endTokens() const override @@ -175,12 +161,10 @@ class ChatAPI : public QObject, public LLModel { { throwNotImplemented(); } private: - std::function m_responseCallback; - QString m_modelName; - QString m_apiKey; - QString m_requestURL; - QList m_context; - QStringList m_queuedPrompts; + ResponseCallback m_responseCallback; + QString m_modelName; + QString m_apiKey; + QString m_requestURL; }; #endif // CHATAPI_H diff --git a/gpt4all-chat/src/chatlistmodel.cpp b/gpt4all-chat/src/chatlistmodel.cpp index 207a2b3b7e8b..85cb44d5fdb5 100644 --- a/gpt4all-chat/src/chatlistmodel.cpp +++ b/gpt4all-chat/src/chatlistmodel.cpp @@ -17,9 +17,10 @@ #include #include +#include -#define CHAT_FORMAT_MAGIC 0xF5D553CC -#define CHAT_FORMAT_VERSION 10 +static constexpr quint32 CHAT_FORMAT_MAGIC = 0xF5D553CC; +static constexpr qint32 CHAT_FORMAT_VERSION = 12; class MyChatListModel: public ChatListModel { }; Q_GLOBAL_STATIC(MyChatListModel, chatListModelInstance) @@ -51,9 +52,11 @@ void ChatListModel::loadChats() connect(thread, &ChatsRestoreThread::finished, thread, &QObject::deleteLater); thread->start(); - ChatSaver *saver = new ChatSaver; - connect(this, &ChatListModel::requestSaveChats, saver, &ChatSaver::saveChats, Qt::QueuedConnection); - connect(saver, &ChatSaver::saveChatsFinished, this, &ChatListModel::saveChatsFinished, Qt::QueuedConnection); + m_chatSaver = std::make_unique(); + connect(this, &ChatListModel::requestSaveChats, m_chatSaver.get(), &ChatSaver::saveChats, Qt::QueuedConnection); + connect(m_chatSaver.get(), &ChatSaver::saveChatsFinished, this, &ChatListModel::saveChatsFinished, Qt::QueuedConnection); + // save chats on application quit + connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, &ChatListModel::saveChatsSync); connect(MySettings::globalInstance(), &MySettings::serverChatChanged, this, &ChatListModel::handleServerEnabledChanged); } @@ -77,16 +80,24 @@ ChatSaver::ChatSaver() m_thread.start(); } +ChatSaver::~ChatSaver() +{ + m_thread.quit(); + m_thread.wait(); +} + +QVector ChatListModel::getChatsToSave() const +{ + QVector toSave; + for (auto *chat : m_chats) + if (chat != m_serverChat && !chat->isNewChat()) + toSave << chat; + return toSave; +} + void ChatListModel::saveChats() { - QVector toSave; - for (Chat *chat : m_chats) { - if (chat == m_serverChat) - continue; - if (chat->isNewChat()) - continue; - toSave.append(chat); - } + auto toSave = getChatsToSave(); if (toSave.isEmpty()) { emit saveChatsFinished(); return; @@ -95,8 +106,24 @@ void ChatListModel::saveChats() emit requestSaveChats(toSave); } +void ChatListModel::saveChatsForQuit() +{ + saveChats(); + m_startedFinalSave = true; +} + +void ChatListModel::saveChatsSync() +{ + auto toSave = getChatsToSave(); + if (!m_startedFinalSave && !toSave.isEmpty()) + m_chatSaver->saveChats(toSave); +} + void ChatSaver::saveChats(const QVector &chats) { + // we can be called from the main thread instead of a worker thread at quit time, so take a lock + QMutexLocker locker(&m_mutex); + QElapsedTimer timer; timer.start(); const QString savePath = MySettings::globalInstance()->modelPath(); @@ -118,8 +145,8 @@ void ChatSaver::saveChats(const QVector &chats) } QDataStream out(&tempFile); - out << (quint32)CHAT_FORMAT_MAGIC; - out << (qint32)CHAT_FORMAT_VERSION; + out << CHAT_FORMAT_MAGIC; + out << CHAT_FORMAT_VERSION; out.setVersion(QDataStream::Qt_6_2); qDebug() << "serializing chat" << fileName; @@ -257,12 +284,15 @@ void ChatsRestoreThread::run() qDebug() << "deserializing chat" << f.file; - Chat *chat = new Chat; + auto chat = std::make_unique(); chat->moveToThread(qGuiApp->thread()); - if (!chat->deserialize(in, version)) { + bool ok = chat->deserialize(in, version); + if (!ok) { qWarning() << "ERROR: Couldn't deserialize chat from file:" << file.fileName(); + } else if (!in.atEnd()) { + qWarning().nospace() << "error loading chat from " << file.fileName() << ": extra data at end of file"; } else { - emit chatRestored(chat); + emit chatRestored(chat.release()); } if (f.oldFile) file.remove(); // No longer storing in this directory diff --git a/gpt4all-chat/src/chatlistmodel.h b/gpt4all-chat/src/chatlistmodel.h index 4fe1c374e0ce..0c405b152381 100644 --- a/gpt4all-chat/src/chatlistmodel.h +++ b/gpt4all-chat/src/chatlistmodel.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,9 @@ #include #include +#include + + class ChatsRestoreThread : public QThread { Q_OBJECT @@ -33,6 +37,7 @@ class ChatSaver : public QObject Q_OBJECT public: explicit ChatSaver(); + ~ChatSaver() override; Q_SIGNALS: void saveChatsFinished(); @@ -42,6 +47,7 @@ public Q_SLOTS: private: QThread m_thread; + QMutex m_mutex; }; class ChatListModel : public QAbstractListModel @@ -228,6 +234,7 @@ class ChatListModel : public QAbstractListModel void removeChatFile(Chat *chat) const; Q_INVOKABLE void saveChats(); + Q_INVOKABLE void saveChatsForQuit(); void restoreChat(Chat *chat); void chatsRestoredFinished(); @@ -244,6 +251,9 @@ public Q_SLOTS: bool eventFilter(QObject *obj, QEvent *ev) override; private Q_SLOTS: + // Used with QCoreApplication::aboutToQuit. Does not require an event loop. + void saveChatsSync(); + void newChatCountChanged() { Q_ASSERT(m_newChat && m_newChat->chatModel()->count()); @@ -274,11 +284,16 @@ private Q_SLOTS: } } +private: + QVector getChatsToSave() const; + private: Chat* m_newChat = nullptr; Chat* m_serverChat = nullptr; Chat* m_currentChat = nullptr; QList m_chats; + std::unique_ptr m_chatSaver; + bool m_startedFinalSave = false; private: explicit ChatListModel(); diff --git a/gpt4all-chat/src/chatllm.cpp b/gpt4all-chat/src/chatllm.cpp index e1cae8c8c1c9..f8ac85a46107 100644 --- a/gpt4all-chat/src/chatllm.cpp +++ b/gpt4all-chat/src/chatllm.cpp @@ -3,9 +3,17 @@ #include "chat.h" #include "chatapi.h" #include "chatmodel.h" +#include "jinja_helpers.h" #include "localdocs.h" #include "mysettings.h" #include "network.h" +#include "tool.h" +#include "toolmodel.h" +#include "toolcallparser.h" + +#include +#include +#include #include #include @@ -17,34 +25,72 @@ #include #include #include -#include +#include // IWYU pragma: keep #include +#include #include -#include #include #include #include #include #include -#include +#include #include #include -#include +#include +#include +#include #include #include +#include +#include #include +#include +#include #include +#include #include #include using namespace Qt::Literals::StringLiterals; +using namespace ToolEnums; +namespace ranges = std::ranges; +using json = nlohmann::ordered_json; //#define DEBUG //#define DEBUG_MODEL_LOADING -static constexpr int LLAMA_INTERNAL_STATE_VERSION = 0; -static constexpr int API_INTERNAL_STATE_VERSION = 0; +// NOTE: not threadsafe +static const std::shared_ptr &jinjaEnv() +{ + static std::shared_ptr environment; + if (!environment) { + environment = minja::Context::builtins(); + environment->set("strftime_now", minja::simple_function( + "strftime_now", { "format" }, + [](const std::shared_ptr &, minja::Value &args) -> minja::Value { + auto format = args.at("format").get(); + using Clock = std::chrono::system_clock; + time_t nowUnix = Clock::to_time_t(Clock::now()); + auto localDate = *std::localtime(&nowUnix); + std::ostringstream ss; + ss << std::put_time(&localDate, format.c_str()); + return ss.str(); + } + )); + environment->set("regex_replace", minja::simple_function( + "regex_replace", { "str", "pattern", "repl" }, + [](const std::shared_ptr &, minja::Value &args) -> minja::Value { + auto str = args.at("str" ).get(); + auto pattern = args.at("pattern").get(); + auto repl = args.at("repl" ).get(); + return std::regex_replace(str, std::regex(pattern), repl); + } + )); + } + return environment; +} class LLModelStore { public: @@ -107,9 +153,6 @@ void LLModelInfo::resetModel(ChatLLM *cllm, LLModel *model) { ChatLLM::ChatLLM(Chat *parent, bool isServer) : QObject{nullptr} , m_chat(parent) - , m_promptResponseTokens(0) - , m_promptTokens(0) - , m_restoringFromText(false) , m_shouldBeLoaded(false) , m_forceUnloadModel(false) , m_markedForDeletion(false) @@ -118,8 +161,6 @@ ChatLLM::ChatLLM(Chat *parent, bool isServer) , m_isServer(isServer) , m_forceMetal(MySettings::globalInstance()->forceMetal()) , m_reloadingToChangeVariant(false) - , m_processedSystemPrompt(false) - , m_restoreStateFromText(false) , m_chatModel(parent->chatModel()) { moveToThread(&m_llmThread); @@ -241,12 +282,8 @@ void ChatLLM::trySwitchContextOfLoadedModel(const ModelInfo &modelInfo) #endif emit trySwitchContextOfLoadedModelCompleted(2); - - // Restore, signal and process - restoreState(); emit modelLoadingPercentageChanged(1.0f); emit trySwitchContextOfLoadedModelCompleted(0); - processSystemPrompt(); } bool ChatLLM::loadModel(const ModelInfo &modelInfo) @@ -260,15 +297,13 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo) // to provide an overview of what we're doing here. if (isModelLoaded() && this->modelInfo() == modelInfo) { - // already acquired -> keep it and reset - resetContext(); + // already acquired -> keep it return true; // already loaded } // reset status emit modelLoadingPercentageChanged(std::numeric_limits::min()); // small non-zero positive value emit modelLoadingError(""); - m_pristineLoadedState = false; QString filePath = modelInfo.dirpath + modelInfo.filename(); QFileInfo fileInfo(filePath); @@ -276,7 +311,6 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo) // We have a live model, but it isn't the one we want bool alreadyAcquired = isModelLoaded(); if (alreadyAcquired) { - resetContext(); #if defined(DEBUG_MODEL_LOADING) qDebug() << "already acquired model deleted" << m_llmThread.objectName() << m_llModelInfo.model.get(); #endif @@ -306,14 +340,11 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo) #if defined(DEBUG_MODEL_LOADING) qDebug() << "store had our model" << m_llmThread.objectName() << m_llModelInfo.model.get(); #endif - restoreState(); emit modelLoadingPercentageChanged(1.0f); setModelInfo(modelInfo); Q_ASSERT(!m_modelInfo.filename().isEmpty()); if (m_modelInfo.filename().isEmpty()) emit modelLoadingError(u"Modelinfo is left null for %1"_s.arg(modelInfo.filename())); - else - processSystemPrompt(); return true; } else { // Release the memory since we have to switch to a different model. @@ -371,7 +402,6 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo) #if defined(DEBUG_MODEL_LOADING) qDebug() << "new model" << m_llmThread.objectName() << m_llModelInfo.model.get(); #endif - restoreState(); #if defined(DEBUG) qDebug() << "modelLoadedChanged" << m_llmThread.objectName(); fflush(stdout); @@ -389,10 +419,8 @@ bool ChatLLM::loadModel(const ModelInfo &modelInfo) emit modelLoadingError(u"Could not find file for model %1"_s.arg(modelInfo.filename())); } - if (m_llModelInfo.model) { + if (m_llModelInfo.model) setModelInfo(modelInfo); - processSystemPrompt(); - } return bool(m_llModelInfo.model); } @@ -594,71 +622,33 @@ bool ChatLLM::isModelLoaded() const return m_llModelInfo.model && m_llModelInfo.model->isModelLoaded(); } -std::string remove_leading_whitespace(const std::string& input) -{ - auto first_non_whitespace = std::find_if(input.begin(), input.end(), [](unsigned char c) { - return !std::isspace(c); - }); - - if (first_non_whitespace == input.end()) - return std::string(); - - return std::string(first_non_whitespace, input.end()); -} - -std::string trim_whitespace(const std::string& input) -{ - auto first_non_whitespace = std::find_if(input.begin(), input.end(), [](unsigned char c) { - return !std::isspace(c); - }); - - if (first_non_whitespace == input.end()) - return std::string(); - - auto last_non_whitespace = std::find_if(input.rbegin(), input.rend(), [](unsigned char c) { - return !std::isspace(c); - }).base(); - - return std::string(first_non_whitespace, last_non_whitespace); -} - -// FIXME(jared): we don't actually have to re-decode the prompt to generate a new response -void ChatLLM::regenerateResponse() +static QString &removeLeadingWhitespace(QString &s) { - // ChatGPT uses a different semantic meaning for n_past than local models. For ChatGPT, the meaning - // of n_past is of the number of prompt/response pairs, rather than for total tokens. - if (m_llModelType == LLModelTypeV1::API) - m_ctx.n_past -= 1; - else - m_ctx.n_past -= m_promptResponseTokens; - m_ctx.n_past = std::max(0, m_ctx.n_past); - m_promptResponseTokens = 0; - m_promptTokens = 0; - m_response = m_trimmedResponse = std::string(); - emit responseChanged(QString::fromStdString(m_trimmedResponse)); + auto firstNonSpace = ranges::find_if_not(s, [](auto c) { return c.isSpace(); }); + s.remove(0, firstNonSpace - s.begin()); + return s; } -void ChatLLM::resetResponse() +template + requires std::convertible_to, QChar> +bool isAllSpace(R &&r) { - m_promptTokens = 0; - m_promptResponseTokens = 0; - m_response = m_trimmedResponse = std::string(); - emit responseChanged(QString::fromStdString(m_trimmedResponse)); + return ranges::all_of(std::forward(r), [](QChar c) { return c.isSpace(); }); } -void ChatLLM::resetContext() +void ChatLLM::regenerateResponse(int index) { - resetResponse(); - m_processedSystemPrompt = false; - m_ctx = LLModel::PromptContext(); + Q_ASSERT(m_chatModel); + if (m_chatModel->regenerateResponse(index)) { + emit responseChanged(); + prompt(m_chat->collectionList()); + } } -QString ChatLLM::response(bool trim) const +std::optional ChatLLM::popPrompt(int index) { - std::string resp = m_response; - if (trim) - resp = remove_leading_whitespace(resp); - return QString::fromStdString(resp); + Q_ASSERT(m_chatModel); + return m_chatModel->popPrompt(index); } ModelInfo ChatLLM::modelInfo() const @@ -693,148 +683,348 @@ void ChatLLM::modelChangeRequested(const ModelInfo &modelInfo) } } -bool ChatLLM::handlePrompt(int32_t token) +static LLModel::PromptContext promptContextFromSettings(const ModelInfo &modelInfo) { - // m_promptResponseTokens is related to last prompt/response not - // the entire context window which we can reset on regenerate prompt -#if defined(DEBUG) - qDebug() << "prompt process" << m_llmThread.objectName() << token; -#endif - ++m_promptTokens; - ++m_promptResponseTokens; - m_timer->start(); - return !m_stopGenerating; + auto *mySettings = MySettings::globalInstance(); + return { + .n_predict = mySettings->modelMaxLength (modelInfo), + .top_k = mySettings->modelTopK (modelInfo), + .top_p = float(mySettings->modelTopP (modelInfo)), + .min_p = float(mySettings->modelMinP (modelInfo)), + .temp = float(mySettings->modelTemperature (modelInfo)), + .n_batch = mySettings->modelPromptBatchSize (modelInfo), + .repeat_penalty = float(mySettings->modelRepeatPenalty(modelInfo)), + .repeat_last_n = mySettings->modelRepeatPenaltyTokens(modelInfo), + }; } -bool ChatLLM::handleResponse(int32_t token, const std::string &response) +void ChatLLM::prompt(const QStringList &enabledCollections) { -#if defined(DEBUG) - printf("%s", response.c_str()); - fflush(stdout); -#endif + if (!isModelLoaded()) { + emit responseStopped(0); + return; + } - // check for error - // FIXME (Adam) The error messages should not be treated as a model response or part of the - // normal conversation. They should be serialized along with the conversation, but the strings - // are separate and we should preserve info that these are error messages and not actual model responses. - if (token < 0) { - m_response.append(response); - m_trimmedResponse = remove_leading_whitespace(m_response); - emit responseChanged(QString::fromStdString(m_trimmedResponse)); - return false; + try { + promptInternalChat(enabledCollections, promptContextFromSettings(m_modelInfo)); + } catch (const std::exception &e) { + // FIXME(jared): this is neither translated nor serialized + m_chatModel->setResponseValue(u"Error: %1"_s.arg(QString::fromUtf8(e.what()))); + m_chatModel->setError(); + emit responseStopped(0); } +} - // m_promptResponseTokens is related to last prompt/response not - // the entire context window which we can reset on regenerate prompt - ++m_promptResponseTokens; - m_timer->inc(); - Q_ASSERT(!response.empty()); - m_response.append(response); - m_trimmedResponse = remove_leading_whitespace(m_response); - emit responseChanged(QString::fromStdString(m_trimmedResponse)); - return !m_stopGenerating; +std::vector ChatLLM::forkConversation(const QString &prompt) const +{ + Q_ASSERT(m_chatModel); + if (m_chatModel->hasError()) + throw std::logic_error("cannot continue conversation with an error"); + + std::vector conversation; + { + auto items = m_chatModel->messageItems(); + // It is possible the main thread could have erased the conversation while the llm thread, + // is busy forking the conversatoin but it must have set stop generating first + Q_ASSERT(items.size() >= 2 || m_stopGenerating); // should be prompt/response pairs + conversation.reserve(items.size() + 1); + conversation.assign(items.begin(), items.end()); + } + qsizetype nextIndex = conversation.empty() ? 0 : conversation.back().index().value() + 1; + conversation.emplace_back(nextIndex, MessageItem::Type::Prompt, prompt.toUtf8()); + return conversation; } -bool ChatLLM::prompt(const QList &collectionList, const QString &prompt) +// version 0 (default): HF compatible +// version 1: explicit LocalDocs formatting +static uint parseJinjaTemplateVersion(QStringView tmpl) { - if (m_restoreStateFromText) { - Q_ASSERT(m_state.isEmpty()); - processRestoreStateFromText(); + static uint MAX_VERSION = 1; + static QRegularExpression reVersion(uR"(\A{#-?\s+gpt4all v(\d+)-?#}\s*$)"_s, QRegularExpression::MultilineOption); + if (auto match = reVersion.matchView(tmpl); match.hasMatch()) { + uint ver = match.captured(1).toUInt(); + if (ver > MAX_VERSION) + throw std::out_of_range(fmt::format("Unknown template version: {}", ver)); + return ver; } + return 0; +} - const QString promptTemplate = MySettings::globalInstance()->modelPromptTemplate(m_modelInfo); - const int32_t n_predict = MySettings::globalInstance()->modelMaxLength(m_modelInfo); - const int32_t top_k = MySettings::globalInstance()->modelTopK(m_modelInfo); - const float top_p = MySettings::globalInstance()->modelTopP(m_modelInfo); - const float min_p = MySettings::globalInstance()->modelMinP(m_modelInfo); - const float temp = MySettings::globalInstance()->modelTemperature(m_modelInfo); - const int32_t n_batch = MySettings::globalInstance()->modelPromptBatchSize(m_modelInfo); - const float repeat_penalty = MySettings::globalInstance()->modelRepeatPenalty(m_modelInfo); - const int32_t repeat_penalty_tokens = MySettings::globalInstance()->modelRepeatPenaltyTokens(m_modelInfo); - return promptInternal(collectionList, prompt, promptTemplate, n_predict, top_k, top_p, min_p, temp, n_batch, - repeat_penalty, repeat_penalty_tokens); +static std::shared_ptr loadJinjaTemplate(const std::string &source) +{ + return minja::Parser::parse(source, { .trim_blocks = true, .lstrip_blocks = true, .keep_trailing_newline = false }); } -bool ChatLLM::promptInternal(const QList &collectionList, const QString &prompt, const QString &promptTemplate, - int32_t n_predict, int32_t top_k, float top_p, float min_p, float temp, int32_t n_batch, float repeat_penalty, - int32_t repeat_penalty_tokens, std::optional fakeReply) +std::optional ChatLLM::checkJinjaTemplateError(const std::string &source) { - if (!isModelLoaded()) - return false; + try { + loadJinjaTemplate(source); + } catch (const std::runtime_error &e) { + return e.what(); + } + return std::nullopt; +} + +std::string ChatLLM::applyJinjaTemplate(std::span items) const +{ + Q_ASSERT(items.size() >= 1); + + auto *mySettings = MySettings::globalInstance(); + auto &model = m_llModelInfo.model; + + QString chatTemplate, systemMessage; + auto chatTemplateSetting = mySettings->modelChatTemplate(m_modelInfo); + if (auto tmpl = chatTemplateSetting.asModern()) { + chatTemplate = *tmpl; + } else if (chatTemplateSetting.isLegacy()) { + throw std::logic_error("cannot apply Jinja to a legacy prompt template"); + } else { + throw std::logic_error("cannot apply Jinja without setting a chat template first"); + } + if (isAllSpace(chatTemplate)) { + throw std::logic_error("cannot apply Jinja with a blank chat template"); + } + if (auto tmpl = mySettings->modelSystemMessage(m_modelInfo).asModern()) { + systemMessage = *tmpl; + } else { + throw std::logic_error("cannot apply Jinja with a legacy system message"); + } - if (!m_processedSystemPrompt) - processSystemPrompt(); + uint version = parseJinjaTemplateVersion(chatTemplate); + + auto makeMap = [version](const MessageItem &item) { + return JinjaMessage(version, item).AsJson(); + }; + + std::unique_ptr systemItem; + bool useSystem = !isAllSpace(systemMessage); + + json::array_t messages; + messages.reserve(useSystem + items.size()); + if (useSystem) { + systemItem = std::make_unique(MessageItem::system_tag, systemMessage.toUtf8()); + messages.emplace_back(makeMap(*systemItem)); + } + for (auto &item : items) + messages.emplace_back(makeMap(item)); + + json::array_t toolList; + const int toolCount = ToolModel::globalInstance()->count(); + for (int i = 0; i < toolCount; ++i) { + Tool *t = ToolModel::globalInstance()->get(i); + toolList.push_back(t->jinjaValue()); + } + + json::object_t params { + { "messages", std::move(messages) }, + { "add_generation_prompt", true }, + { "toolList", toolList }, + }; + for (auto &[name, token] : model->specialTokens()) + params.emplace(std::move(name), std::move(token)); + + try { + auto tmpl = loadJinjaTemplate(chatTemplate.toStdString()); + auto context = minja::Context::make(minja::Value(std::move(params)), jinjaEnv()); + return tmpl->render(context); + } catch (const std::runtime_error &e) { + throw std::runtime_error(fmt::format("Failed to parse chat template: {}", e.what())); + } + Q_UNREACHABLE(); +} + +auto ChatLLM::promptInternalChat(const QStringList &enabledCollections, const LLModel::PromptContext &ctx, + qsizetype startOffset) -> ChatPromptResult +{ + Q_ASSERT(isModelLoaded()); + Q_ASSERT(m_chatModel); + + // Return a vector of relevant messages for this chat. + // "startOffset" is used to select only local server messages from the current chat session. + auto getChat = [&]() { + auto items = m_chatModel->messageItems(); + if (startOffset > 0) + items.erase(items.begin(), items.begin() + startOffset); + Q_ASSERT(items.size() >= 2); + return items; + }; QList databaseResults; - const int retrievalSize = MySettings::globalInstance()->localDocsRetrievalSize(); - if (!fakeReply && !collectionList.isEmpty()) { - emit requestRetrieveFromDB(collectionList, prompt, retrievalSize, &databaseResults); // blocks - emit databaseResultsChanged(databaseResults); + if (!enabledCollections.isEmpty()) { + std::optional> query; + { + // Find the prompt that represents the query. Server chats are flexible and may not have one. + auto items = getChat(); + if (auto peer = m_chatModel->getPeer(items, items.end() - 1)) // peer of response + query = { (*peer)->index().value(), (*peer)->content() }; + } + + if (query) { + auto &[promptIndex, queryStr] = *query; + const int retrievalSize = MySettings::globalInstance()->localDocsRetrievalSize(); + emit requestRetrieveFromDB(enabledCollections, queryStr, retrievalSize, &databaseResults); // blocks + m_chatModel->updateSources(promptIndex, databaseResults); + emit databaseResultsChanged(databaseResults); + } } - // Augment the prompt template with the results if any - QString docsContext; - if (!databaseResults.isEmpty()) { - QStringList results; - for (const ResultInfo &info : databaseResults) - results << u"Collection: %1\nPath: %2\nExcerpt: %3"_s.arg(info.collection, info.path, info.text); + auto messageItems = getChat(); + messageItems.pop_back(); // exclude new response + + auto result = promptInternal(messageItems, ctx, !databaseResults.isEmpty()); + return { + /*PromptResult*/ { + .response = std::move(result.response), + .promptTokens = result.promptTokens, + .responseTokens = result.responseTokens, + }, + /*databaseResults*/ std::move(databaseResults), + }; +} + +auto ChatLLM::promptInternal( + const std::variant, std::string_view> &prompt, + const LLModel::PromptContext &ctx, + bool usedLocalDocs +) -> PromptResult +{ + Q_ASSERT(isModelLoaded()); + + auto *mySettings = MySettings::globalInstance(); - // FIXME(jared): use a Jinja prompt template instead of hardcoded Alpaca-style localdocs template - docsContext = u"### Context:\n%1\n\n"_s.arg(results.join("\n\n")); + // unpack prompt argument + const std::span *messageItems = nullptr; + std::string jinjaBuffer; + std::string_view conversation; + if (auto *nonChat = std::get_if(&prompt)) { + conversation = *nonChat; // complete the string without a template + } else { + messageItems = &std::get>(prompt); + jinjaBuffer = applyJinjaTemplate(*messageItems); + conversation = jinjaBuffer; } - int n_threads = MySettings::globalInstance()->threadCount(); - - m_stopGenerating = false; - auto promptFunc = std::bind(&ChatLLM::handlePrompt, this, std::placeholders::_1); - auto responseFunc = std::bind(&ChatLLM::handleResponse, this, std::placeholders::_1, - std::placeholders::_2); - emit promptProcessing(); - m_ctx.n_predict = n_predict; - m_ctx.top_k = top_k; - m_ctx.top_p = top_p; - m_ctx.min_p = min_p; - m_ctx.temp = temp; - m_ctx.n_batch = n_batch; - m_ctx.repeat_penalty = repeat_penalty; - m_ctx.repeat_last_n = repeat_penalty_tokens; - m_llModelInfo.model->setThreadCount(n_threads); -#if defined(DEBUG) - printf("%s", qPrintable(prompt)); - fflush(stdout); -#endif + // check for overlength last message + if (!dynamic_cast(m_llModelInfo.model.get())) { + auto nCtx = m_llModelInfo.model->contextLength(); + std::string jinjaBuffer2; + auto lastMessageRendered = (messageItems && messageItems->size() > 1) + ? std::string_view(jinjaBuffer2 = applyJinjaTemplate({ &messageItems->back(), 1 })) + : conversation; + int32_t lastMessageLength = m_llModelInfo.model->countPromptTokens(lastMessageRendered); + if (auto limit = nCtx - 4; lastMessageLength > limit) { + throw std::invalid_argument( + tr("Your message was too long and could not be processed (%1 > %2). " + "Please try again with something shorter.").arg(lastMessageLength).arg(limit).toUtf8().constData() + ); + } + } + + PromptResult result {}; + + auto handlePrompt = [this, &result](std::span batch, bool cached) -> bool { + Q_UNUSED(cached) + result.promptTokens += batch.size(); + m_timer->start(); + return !m_stopGenerating; + }; + QElapsedTimer totalTime; totalTime.start(); m_timer->start(); - if (!docsContext.isEmpty()) { - auto old_n_predict = std::exchange(m_ctx.n_predict, 0); // decode localdocs context without a response - m_llModelInfo.model->prompt(docsContext.toStdString(), "%1", promptFunc, responseFunc, - /*allowContextShift*/ true, m_ctx); - m_ctx.n_predict = old_n_predict; // now we are ready for a response + + ToolCallParser toolCallParser; + auto handleResponse = [this, &result, &toolCallParser, &totalTime](LLModel::Token token, std::string_view piece) -> bool { + Q_UNUSED(token) + result.responseTokens++; + m_timer->inc(); + + toolCallParser.update(piece.data()); + + // Split the response into two if needed and create chat items + if (toolCallParser.numberOfBuffers() < 2 && toolCallParser.splitIfPossible()) { + const auto parseBuffers = toolCallParser.buffers(); + Q_ASSERT(parseBuffers.size() == 2); + if (toolCallParser.startTag() == ToolCallConstants::ThinkTag) + m_chatModel->splitThinking({parseBuffers.at(0), parseBuffers.at(1)}); + else + m_chatModel->splitToolCall({parseBuffers.at(0), parseBuffers.at(1)}); + } + + // Split the response into three if needed and create chat items + if (toolCallParser.numberOfBuffers() < 3 && toolCallParser.startTag() == ToolCallConstants::ThinkTag + && toolCallParser.splitIfPossible()) { + const auto parseBuffers = toolCallParser.buffers(); + Q_ASSERT(parseBuffers.size() == 3); + m_chatModel->endThinking({parseBuffers.at(1), parseBuffers.at(2)}, totalTime.elapsed()); + } + + result.response.append(piece.data(), piece.size()); + auto respStr = QString::fromUtf8(result.response); + + try { + const auto parseBuffers = toolCallParser.buffers(); + if (parseBuffers.size() > 1) + m_chatModel->setResponseValue(parseBuffers.last()); + else + m_chatModel->setResponseValue(removeLeadingWhitespace(respStr)); + } catch (const std::exception &e) { + // We have a try/catch here because the main thread might have removed the response from + // the chatmodel by erasing the conversation during the response... the main thread sets + // m_stopGenerating before doing so, but it doesn't wait after that to reset the chatmodel + Q_ASSERT(m_stopGenerating); + return false; + } + + emit responseChanged(); + + const bool shouldExecuteToolCall = toolCallParser.state() == ToolEnums::ParseState::Complete + && toolCallParser.startTag() != ToolCallConstants::ThinkTag; + + return !shouldExecuteToolCall && !m_stopGenerating; + }; + + try { + emit promptProcessing(); + m_llModelInfo.model->setThreadCount(mySettings->threadCount()); + m_stopGenerating = false; + m_llModelInfo.model->prompt(conversation, handlePrompt, handleResponse, ctx); + } catch (...) { + m_timer->stop(); + throw; } - m_llModelInfo.model->prompt(prompt.toStdString(), promptTemplate.toStdString(), promptFunc, responseFunc, - /*allowContextShift*/ true, m_ctx, false, - fakeReply.transform(std::mem_fn(&QString::toStdString))); -#if defined(DEBUG) - printf("\n"); - fflush(stdout); -#endif + m_timer->stop(); qint64 elapsed = totalTime.elapsed(); - std::string trimmed = trim_whitespace(m_response); - if (trimmed != m_trimmedResponse) { - m_trimmedResponse = trimmed; - emit responseChanged(QString::fromStdString(m_trimmedResponse)); + + const auto parseBuffers = toolCallParser.buffers(); + const bool shouldExecuteToolCall = toolCallParser.state() == ToolEnums::ParseState::Complete + && toolCallParser.startTag() != ToolCallConstants::ThinkTag; + + // trim trailing whitespace + auto respStr = QString::fromUtf8(result.response); + if (!respStr.isEmpty() && (std::as_const(respStr).back().isSpace() || parseBuffers.size() > 1)) { + if (parseBuffers.size() > 1) + m_chatModel->setResponseValue(parseBuffers.last()); + else + m_chatModel->setResponseValue(respStr.trimmed()); + emit responseChanged(); } - SuggestionMode mode = MySettings::globalInstance()->suggestionMode(); - if (mode == SuggestionMode::On || (!databaseResults.isEmpty() && mode == SuggestionMode::LocalDocsOnly)) + bool doQuestions = false; + if (!m_isServer && messageItems && !shouldExecuteToolCall) { + switch (mySettings->suggestionMode()) { + case SuggestionMode::On: doQuestions = true; break; + case SuggestionMode::LocalDocsOnly: doQuestions = usedLocalDocs; break; + case SuggestionMode::Off: ; + } + } + if (doQuestions) generateQuestions(elapsed); else emit responseStopped(elapsed); - m_pristineLoadedState = false; - return true; + return result; } void ChatLLM::setShouldBeLoaded(bool b) @@ -870,9 +1060,6 @@ void ChatLLM::unloadModel() else emit modelLoadingPercentageChanged(std::numeric_limits::min()); // small non-zero positive value - if (!m_markedForDeletion) - saveState(); - #if defined(DEBUG_MODEL_LOADING) qDebug() << "unloadModel" << m_llmThread.objectName() << m_llModelInfo.model.get(); #endif @@ -883,7 +1070,6 @@ void ChatLLM::unloadModel() } LLModelStore::globalInstance()->releaseModel(std::move(m_llModelInfo)); - m_pristineLoadedState = false; } void ChatLLM::reloadModel() @@ -907,478 +1093,201 @@ void ChatLLM::reloadModel() void ChatLLM::generateName() { Q_ASSERT(isModelLoaded()); - if (!isModelLoaded()) + if (!isModelLoaded() || m_isServer) return; - const QString chatNamePrompt = MySettings::globalInstance()->modelChatNamePrompt(m_modelInfo); - if (chatNamePrompt.trimmed().isEmpty()) { + Q_ASSERT(m_chatModel); + + auto *mySettings = MySettings::globalInstance(); + + const QString chatNamePrompt = mySettings->modelChatNamePrompt(m_modelInfo); + if (isAllSpace(chatNamePrompt)) { qWarning() << "ChatLLM: not generating chat name because prompt is empty"; return; } - auto promptTemplate = MySettings::globalInstance()->modelPromptTemplate(m_modelInfo); - auto promptFunc = std::bind(&ChatLLM::handleNamePrompt, this, std::placeholders::_1); - auto responseFunc = std::bind(&ChatLLM::handleNameResponse, this, std::placeholders::_1, std::placeholders::_2); - LLModel::PromptContext ctx = m_ctx; - m_llModelInfo.model->prompt(chatNamePrompt.toStdString(), promptTemplate.toStdString(), - promptFunc, responseFunc, /*allowContextShift*/ false, ctx); - std::string trimmed = trim_whitespace(m_nameResponse); - if (trimmed != m_nameResponse) { - m_nameResponse = trimmed; - emit generatedNameChanged(QString::fromStdString(m_nameResponse)); - } - m_pristineLoadedState = false; -} + QByteArray response; // raw UTF-8 -void ChatLLM::handleChatIdChanged(const QString &id) -{ - m_llmThread.setObjectName(id); -} + auto handleResponse = [this, &response](LLModel::Token token, std::string_view piece) -> bool { + Q_UNUSED(token) -bool ChatLLM::handleNamePrompt(int32_t token) -{ -#if defined(DEBUG) - qDebug() << "name prompt" << m_llmThread.objectName() << token; -#endif - Q_UNUSED(token); - return !m_stopGenerating; -} - -bool ChatLLM::handleNameResponse(int32_t token, const std::string &response) -{ -#if defined(DEBUG) - qDebug() << "name response" << m_llmThread.objectName() << token << response; -#endif - Q_UNUSED(token); + response.append(piece.data(), piece.size()); + QStringList words = QString::fromUtf8(response).simplified().split(u' ', Qt::SkipEmptyParts); + emit generatedNameChanged(words.join(u' ')); + return words.size() <= 3; + }; - m_nameResponse.append(response); - emit generatedNameChanged(QString::fromStdString(m_nameResponse)); - QString gen = QString::fromStdString(m_nameResponse).simplified(); - QStringList words = gen.split(' ', Qt::SkipEmptyParts); - return words.size() <= 3; + try { + m_llModelInfo.model->prompt( + applyJinjaTemplate(forkConversation(chatNamePrompt)), + [this](auto &&...) { return !m_stopGenerating; }, + handleResponse, + promptContextFromSettings(m_modelInfo) + ); + } catch (const std::exception &e) { + qWarning() << "ChatLLM failed to generate name:" << e.what(); + } } -bool ChatLLM::handleQuestionPrompt(int32_t token) +void ChatLLM::handleChatIdChanged(const QString &id) { -#if defined(DEBUG) - qDebug() << "question prompt" << m_llmThread.objectName() << token; -#endif - Q_UNUSED(token); - return !m_stopGenerating; + m_llmThread.setObjectName(id); } -bool ChatLLM::handleQuestionResponse(int32_t token, const std::string &response) +void ChatLLM::generateQuestions(qint64 elapsed) { -#if defined(DEBUG) - qDebug() << "question response" << m_llmThread.objectName() << token << response; -#endif - Q_UNUSED(token); - - // add token to buffer - m_questionResponse.append(response); - - // match whole question sentences // FIXME: This only works with response by the model in english which is not ideal for a multi-language // model. - static const QRegularExpression reQuestion(R"(\b(What|Where|How|Why|When|Who|Which|Whose|Whom)\b[^?]*\?)"); - - // extract all questions from response - int lastMatchEnd = -1; - for (const auto &match : reQuestion.globalMatch(m_questionResponse)) { - lastMatchEnd = match.capturedEnd(); - emit generatedQuestionFinished(match.captured()); - } - - // remove processed input from buffer - if (lastMatchEnd != -1) - m_questionResponse.erase(m_questionResponse.cbegin(), m_questionResponse.cbegin() + lastMatchEnd); - - return true; -} + // match whole question sentences + static const std::regex reQuestion(R"(\b(?:What|Where|How|Why|When|Who|Which|Whose|Whom)\b[^?]*\?)"); -void ChatLLM::generateQuestions(qint64 elapsed) -{ Q_ASSERT(isModelLoaded()); if (!isModelLoaded()) { emit responseStopped(elapsed); return; } - const std::string suggestedFollowUpPrompt = MySettings::globalInstance()->modelSuggestedFollowUpPrompt(m_modelInfo).toStdString(); - if (QString::fromStdString(suggestedFollowUpPrompt).trimmed().isEmpty()) { + auto *mySettings = MySettings::globalInstance(); + + QString suggestedFollowUpPrompt = mySettings->modelSuggestedFollowUpPrompt(m_modelInfo); + if (isAllSpace(suggestedFollowUpPrompt)) { + qWarning() << "ChatLLM: not generating follow-up questions because prompt is empty"; emit responseStopped(elapsed); return; } emit generatingQuestions(); - m_questionResponse.clear(); - auto promptTemplate = MySettings::globalInstance()->modelPromptTemplate(m_modelInfo); - auto promptFunc = std::bind(&ChatLLM::handleQuestionPrompt, this, std::placeholders::_1); - auto responseFunc = std::bind(&ChatLLM::handleQuestionResponse, this, std::placeholders::_1, std::placeholders::_2); - LLModel::PromptContext ctx = m_ctx; - QElapsedTimer totalTime; - totalTime.start(); - m_llModelInfo.model->prompt(suggestedFollowUpPrompt, promptTemplate.toStdString(), promptFunc, responseFunc, - /*allowContextShift*/ false, ctx); - elapsed += totalTime.elapsed(); - emit responseStopped(elapsed); -} + std::string response; // raw UTF-8 -bool ChatLLM::handleSystemPrompt(int32_t token) -{ -#if defined(DEBUG) - qDebug() << "system prompt" << m_llmThread.objectName() << token << m_stopGenerating; -#endif - Q_UNUSED(token); - return !m_stopGenerating; -} - -bool ChatLLM::handleRestoreStateFromTextPrompt(int32_t token) -{ -#if defined(DEBUG) - qDebug() << "restore state from text prompt" << m_llmThread.objectName() << token << m_stopGenerating; -#endif - Q_UNUSED(token); - return !m_stopGenerating; -} + auto handleResponse = [this, &response](LLModel::Token token, std::string_view piece) -> bool { + Q_UNUSED(token) -// this function serialized the cached model state to disk. -// we want to also serialize n_ctx, and read it at load time. -bool ChatLLM::serialize(QDataStream &stream, int version, bool serializeKV) -{ - if (version >= 2) { - if (m_llModelType == LLModelTypeV1::NONE) { - qWarning() << "ChatLLM ERROR: attempted to serialize a null model for chat id" << m_chat->id() - << "name" << m_chat->name(); - return false; - } + // add token to buffer + response.append(piece); - stream << m_llModelType; - switch (m_llModelType) { - case LLModelTypeV1::LLAMA: stream << LLAMA_INTERNAL_STATE_VERSION; break; - case LLModelTypeV1::API: stream << API_INTERNAL_STATE_VERSION; break; - default: stream << 0; // models removed in v2.5.0 + // extract all questions from response + ptrdiff_t lastMatchEnd = -1; + auto it = std::sregex_iterator(response.begin(), response.end(), reQuestion); + auto end = std::sregex_iterator(); + for (; it != end; ++it) { + auto pos = it->position(); + auto len = it->length(); + lastMatchEnd = pos + len; + emit generatedQuestionFinished(QString::fromUtf8(&response[pos], len)); } - } - stream << response(); - stream << generatedName(); - stream << m_promptResponseTokens; - if (!serializeKV) { -#if defined(DEBUG) - qDebug() << "serialize" << m_llmThread.objectName() << m_state.size(); -#endif - return stream.status() == QDataStream::Ok; - } + // remove processed input from buffer + if (lastMatchEnd != -1) + response.erase(0, lastMatchEnd); + return true; + }; - if (version < 4) { - int responseLogits = 0; - stream << responseLogits; - } - stream << m_ctx.n_past; - saveState(); - if (version >= 7) { - stream << m_stateContextLength; + QElapsedTimer totalTime; + totalTime.start(); + try { + m_llModelInfo.model->prompt( + applyJinjaTemplate(forkConversation(suggestedFollowUpPrompt)), + [this](auto &&...) { return !m_stopGenerating; }, + handleResponse, + promptContextFromSettings(m_modelInfo) + ); + } catch (const std::exception &e) { + qWarning() << "ChatLLM failed to generate follow-up questions:" << e.what(); } - stream << quint64(m_stateInputTokens.size()); - stream.writeRawData(reinterpret_cast(m_stateInputTokens.data()), - m_stateInputTokens.size() * sizeof(m_stateInputTokens[0])); - QByteArray compressed = qCompress(m_state); - stream << compressed; -#if defined(DEBUG) - qDebug() << "serialize" << m_llmThread.objectName() << m_state.size(); -#endif - return stream.status() == QDataStream::Ok; + elapsed += totalTime.elapsed(); + emit responseStopped(elapsed); } -bool ChatLLM::deserialize(QDataStream &stream, int version, bool deserializeKV, bool discardKV) +// this function serialized the cached model state to disk. +// we want to also serialize n_ctx, and read it at load time. +bool ChatLLM::serialize(QDataStream &stream, int version) { - if (version >= 2) { - int llModelType; - stream >> llModelType; - m_llModelType = (version >= 6 ? parseLLModelTypeV1 : parseLLModelTypeV0)(llModelType); - if (m_llModelType == LLModelTypeV1::NONE) { - qWarning().nospace() << "error loading chat id " << m_chat->id() << ": unrecognized model type: " - << llModelType; - return false; + if (version < 11) { + if (version >= 6) { + stream << false; // serializeKV } + if (version >= 2) { + if (m_llModelType == LLModelTypeV1::NONE) { + qWarning() << "ChatLLM ERROR: attempted to serialize a null model for chat id" << m_chat->id() + << "name" << m_chat->name(); + return false; + } + stream << m_llModelType; + stream << 0; // state version + } + { + QString dummy; + stream << dummy; // response + stream << dummy; // generated name + } + stream << quint32(0); // prompt + response tokens - /* note: prior to chat version 10, API models and chats with models removed in v2.5.0 only wrote this because of - * undefined behavior in Release builds */ - int internalStateVersion; // for future use - stream >> internalStateVersion; - } - QString response; - stream >> response; - m_response = response.toStdString(); - m_trimmedResponse = trim_whitespace(m_response); - QString nameResponse; - stream >> nameResponse; - m_nameResponse = nameResponse.toStdString(); - stream >> m_promptResponseTokens; - - // If we do not deserialize the KV or it is discarded, then we need to restore the state from the - // text only. This will be a costly operation, but the chat has to be restored from the text archive - // alone. - if (!deserializeKV || discardKV) { - m_restoreStateFromText = true; - m_pristineLoadedState = true; - } - - if (!deserializeKV) { -#if defined(DEBUG) - qDebug() << "deserialize" << m_llmThread.objectName(); -#endif - return stream.status() == QDataStream::Ok; - } - - if (version < 4) { - int responseLogits; - stream >> responseLogits; - } - - int32_t n_past; - stream >> n_past; - if (!discardKV) m_ctx.n_past = n_past; - - if (version >= 7) { - uint32_t n_ctx; - stream >> n_ctx; - if (!discardKV) m_stateContextLength = n_ctx; - } - - if (version < 9) { - quint64 logitsSize; - stream >> logitsSize; - stream.skipRawData(logitsSize * sizeof(float)); - } - - quint64 tokensSize; - stream >> tokensSize; - if (!discardKV) { - m_stateInputTokens.resize(tokensSize); - stream.readRawData(reinterpret_cast(m_stateInputTokens.data()), tokensSize * sizeof(m_stateInputTokens[0])); - } else { - stream.skipRawData(tokensSize * sizeof(m_stateInputTokens[0])); - } - - if (version >= 1) { - QByteArray compressed; - stream >> compressed; - if (!discardKV) - m_state = qUncompress(compressed); - } else { - if (!discardKV) { - stream >> m_state; - } else { - QByteArray state; - stream >> state; + if (version < 6) { // serialize binary state + if (version < 4) { + stream << 0; // responseLogits + } + stream << int32_t(0); // n_past + stream << quint64(0); // input token count + stream << QByteArray(); // KV cache state } } - -#if defined(DEBUG) - qDebug() << "deserialize" << m_llmThread.objectName(); -#endif return stream.status() == QDataStream::Ok; } -void ChatLLM::saveState() +bool ChatLLM::deserialize(QDataStream &stream, int version) { - if (!isModelLoaded() || m_pristineLoadedState) - return; - - if (m_llModelType == LLModelTypeV1::API) { - m_state.clear(); - QDataStream stream(&m_state, QIODeviceBase::WriteOnly); - stream.setVersion(QDataStream::Qt_6_4); - ChatAPI *chatAPI = static_cast(m_llModelInfo.model.get()); - stream << chatAPI->context(); - return; - } - - const size_t stateSize = m_llModelInfo.model->stateSize(); - m_state.resize(stateSize); -#if defined(DEBUG) - qDebug() << "saveState" << m_llmThread.objectName() << "size:" << m_state.size(); -#endif - bool ok = m_llModelInfo.model->saveState({reinterpret_cast(m_state.data()), size_t(m_state.size())}, - m_stateInputTokens); - if (!ok) { - // FIXME(jared): how badly does this situation break GPT4All? - qWarning() << "ChatLLM failed to save LLModel state"; - m_state.clear(); - m_state.squeeze(); - m_stateContextLength = -1; - } - m_stateContextLength = m_llModelInfo.model->contextLength(); -} - -void ChatLLM::restoreState() -{ - if (!isModelLoaded()) - return; - - if (m_llModelType == LLModelTypeV1::API) { - QDataStream stream(m_state); - stream.setVersion(QDataStream::Qt_6_4); - ChatAPI *chatAPI = static_cast(m_llModelInfo.model.get()); - QList context; - stream >> context; - chatAPI->setContext(context); - m_state.clear(); - m_state.squeeze(); - return; - } - -#if defined(DEBUG) - qDebug() << "restoreState" << m_llmThread.objectName() << "size:" << m_state.size(); -#endif - - if (m_state.isEmpty()) - return; + // discard all state since we are initialized from the ChatModel as of v11 + if (version < 11) { + union { int intval; quint32 u32; quint64 u64; }; + + bool deserializeKV = true; + if (version >= 6) + stream >> deserializeKV; + + if (version >= 2) { + stream >> intval; // model type + auto llModelType = (version >= 6 ? parseLLModelTypeV1 : parseLLModelTypeV0)(intval); + if (llModelType == LLModelTypeV1::NONE) { + qWarning().nospace() << "error loading chat id " << m_chat->id() << ": unrecognized model type: " + << intval; + return false; + } - if (m_llModelInfo.model->contextLength() != m_stateContextLength) { - qWarning() << "restoring state from text because of n_ctx mismatch (state" - << m_stateContextLength << "model" << m_llModelInfo.model->contextLength() << ")"; - m_restoreStateFromText = true; - } else { - size_t bytesRead = m_llModelInfo.model->restoreState( - {reinterpret_cast(m_state.data()), size_t(m_state.size())}, - m_stateInputTokens - ); - if (!bytesRead) { - qWarning() << "restoring state from text because of error reading state (mismatch or corrupt data)"; - m_restoreStateFromText = true; - } else { - m_processedSystemPrompt = true; - m_pristineLoadedState = true; + /* note: prior to chat version 10, API models and chats with models removed in v2.5.0 only wrote this because of + * undefined behavior in Release builds */ + stream >> intval; // state version + if (intval) { + qWarning().nospace() << "error loading chat id " << m_chat->id() << ": unrecognized internal state version"; + return false; + } } - } - - // free local state copy unless unload is pending - if (m_shouldBeLoaded) { - m_state.clear(); - m_state.squeeze(); - m_pristineLoadedState = false; - } -} - -void ChatLLM::processSystemPrompt() -{ - Q_ASSERT(isModelLoaded()); - if (!isModelLoaded() || m_processedSystemPrompt) - return; - - const std::string systemPrompt = MySettings::globalInstance()->modelSystemPrompt(m_modelInfo).toStdString(); - - // Start with a whole new context - m_stopGenerating = false; - m_ctx = LLModel::PromptContext(); - - if (!QString::fromStdString(systemPrompt).trimmed().isEmpty()) { - auto promptFunc = std::bind(&ChatLLM::handleSystemPrompt, this, std::placeholders::_1); - - const int32_t n_predict = MySettings::globalInstance()->modelMaxLength(m_modelInfo); - const int32_t top_k = MySettings::globalInstance()->modelTopK(m_modelInfo); - const float top_p = MySettings::globalInstance()->modelTopP(m_modelInfo); - const float min_p = MySettings::globalInstance()->modelMinP(m_modelInfo); - const float temp = MySettings::globalInstance()->modelTemperature(m_modelInfo); - const int32_t n_batch = MySettings::globalInstance()->modelPromptBatchSize(m_modelInfo); - const float repeat_penalty = MySettings::globalInstance()->modelRepeatPenalty(m_modelInfo); - const int32_t repeat_penalty_tokens = MySettings::globalInstance()->modelRepeatPenaltyTokens(m_modelInfo); - int n_threads = MySettings::globalInstance()->threadCount(); - m_ctx.n_predict = n_predict; - m_ctx.top_k = top_k; - m_ctx.top_p = top_p; - m_ctx.min_p = min_p; - m_ctx.temp = temp; - m_ctx.n_batch = n_batch; - m_ctx.repeat_penalty = repeat_penalty; - m_ctx.repeat_last_n = repeat_penalty_tokens; - m_llModelInfo.model->setThreadCount(n_threads); -#if defined(DEBUG) - printf("%s", qPrintable(QString::fromStdString(systemPrompt))); - fflush(stdout); -#endif - auto old_n_predict = std::exchange(m_ctx.n_predict, 0); // decode system prompt without a response - // use "%1%2" and not "%1" to avoid implicit whitespace - m_llModelInfo.model->prompt(systemPrompt, "%1%2", promptFunc, nullptr, /*allowContextShift*/ true, m_ctx, true); - m_ctx.n_predict = old_n_predict; -#if defined(DEBUG) - printf("\n"); - fflush(stdout); -#endif - } - - m_processedSystemPrompt = m_stopGenerating == false; - m_pristineLoadedState = false; -} -void ChatLLM::processRestoreStateFromText() -{ - Q_ASSERT(isModelLoaded()); - if (!isModelLoaded() || !m_restoreStateFromText || m_isServer) - return; - - processSystemPrompt(); - - m_restoringFromText = true; - emit restoringFromTextChanged(); - - m_stopGenerating = false; - - auto promptFunc = std::bind(&ChatLLM::handleRestoreStateFromTextPrompt, this, std::placeholders::_1); - - const QString promptTemplate = MySettings::globalInstance()->modelPromptTemplate(m_modelInfo); - const int32_t n_predict = MySettings::globalInstance()->modelMaxLength(m_modelInfo); - const int32_t top_k = MySettings::globalInstance()->modelTopK(m_modelInfo); - const float top_p = MySettings::globalInstance()->modelTopP(m_modelInfo); - const float min_p = MySettings::globalInstance()->modelMinP(m_modelInfo); - const float temp = MySettings::globalInstance()->modelTemperature(m_modelInfo); - const int32_t n_batch = MySettings::globalInstance()->modelPromptBatchSize(m_modelInfo); - const float repeat_penalty = MySettings::globalInstance()->modelRepeatPenalty(m_modelInfo); - const int32_t repeat_penalty_tokens = MySettings::globalInstance()->modelRepeatPenaltyTokens(m_modelInfo); - int n_threads = MySettings::globalInstance()->threadCount(); - m_ctx.n_predict = n_predict; - m_ctx.top_k = top_k; - m_ctx.top_p = top_p; - m_ctx.min_p = min_p; - m_ctx.temp = temp; - m_ctx.n_batch = n_batch; - m_ctx.repeat_penalty = repeat_penalty; - m_ctx.repeat_last_n = repeat_penalty_tokens; - m_llModelInfo.model->setThreadCount(n_threads); + { + QString dummy; + stream >> dummy; // response + stream >> dummy; // name response + } + stream >> u32; // prompt + response token count - Q_ASSERT(m_chatModel); - m_chatModel->lock(); - auto it = m_chatModel->begin(); - while (it < m_chatModel->end()) { - auto &prompt = *it++; - Q_ASSERT(prompt.name == "Prompt: "); - Q_ASSERT(it < m_chatModel->end()); - - auto &response = *it++; - Q_ASSERT(response.name == "Response: "); - - // FIXME(jared): this doesn't work well with the "regenerate" button since we are not incrementing - // m_promptTokens or m_promptResponseTokens - m_llModelInfo.model->prompt( - prompt.promptPlusAttachments().toStdString(), promptTemplate.toStdString(), - promptFunc, /*responseFunc*/ [](auto &&...) { return true; }, - /*allowContextShift*/ true, - m_ctx, - /*special*/ false, - response.value.toUtf8().constData() - ); + // We don't use the raw model state anymore. + if (deserializeKV) { + if (version < 4) { + stream >> u32; // response logits + } + stream >> u32; // n_past + if (version >= 7) { + stream >> u32; // n_ctx + } + if (version < 9) { + stream >> u64; // logits size + stream.skipRawData(u64 * sizeof(float)); // logits + } + stream >> u64; // token cache size + stream.skipRawData(u64 * sizeof(int)); // token cache + QByteArray dummy; + stream >> dummy; // state + } } - m_chatModel->unlock(); - - if (!m_stopGenerating) - m_restoreStateFromText = false; - - m_restoringFromText = false; - emit restoringFromTextChanged(); - - m_pristineLoadedState = false; + return stream.status() == QDataStream::Ok; } diff --git a/gpt4all-chat/src/chatllm.h b/gpt4all-chat/src/chatllm.h index 4b9936cb038c..e34d3899b0f0 100644 --- a/gpt4all-chat/src/chatllm.h +++ b/gpt4all-chat/src/chatllm.h @@ -1,6 +1,7 @@ #ifndef CHATLLM_H #define CHATLLM_H +#include "chatmodel.h" #include "database.h" // IWYU pragma: keep #include "modellist.h" @@ -13,16 +14,19 @@ #include #include #include +#include // IWYU pragma: keep +#include #include -#include +#include // IWYU pragma: keep #include #include #include #include #include +#include #include -#include +#include using namespace Qt::Literals::StringLiterals; @@ -142,7 +146,6 @@ class Chat; class ChatLLM : public QObject { Q_OBJECT - Q_PROPERTY(bool restoringFromText READ restoringFromText NOTIFY restoringFromTextChanged) Q_PROPERTY(QString deviceBackend READ deviceBackend NOTIFY loadedModelInfoChanged) Q_PROPERTY(QString device READ device NOTIFY loadedModelInfoChanged) Q_PROPERTY(QString fallbackReason READ fallbackReason NOTIFY loadedModelInfoChanged) @@ -150,12 +153,14 @@ class ChatLLM : public QObject ChatLLM(Chat *parent, bool isServer = false); virtual ~ChatLLM(); - void destroy(); static void destroyStore(); + static std::optional checkJinjaTemplateError(const std::string &source); + + void destroy(); bool isModelLoaded() const; - void regenerateResponse(); - void resetResponse(); - void resetContext(); + void regenerateResponse(int index); + // used to implement edit functionality + std::optional popPrompt(int index); void stopGenerating() { m_stopGenerating = true; } @@ -165,13 +170,9 @@ class ChatLLM : public QObject void setForceUnloadModel(bool b) { m_forceUnloadModel = b; } void setMarkedForDeletion(bool b) { m_markedForDeletion = b; } - QString response(bool trim = true) const; - ModelInfo modelInfo() const; void setModelInfo(const ModelInfo &info); - bool restoringFromText() const { return m_restoringFromText; } - void acquireModel(); void resetModel(); @@ -196,13 +197,11 @@ class ChatLLM : public QObject return m_llModelInfo.fallbackReason.value_or(u""_s); } - QString generatedName() const { return QString::fromStdString(m_nameResponse); } - - bool serialize(QDataStream &stream, int version, bool serializeKV); - bool deserialize(QDataStream &stream, int version, bool deserializeKV, bool discardKV); + bool serialize(QDataStream &stream, int version); + bool deserialize(QDataStream &stream, int version); public Q_SLOTS: - bool prompt(const QList &collectionList, const QString &prompt); + void prompt(const QStringList &enabledCollections); bool loadDefaultModel(); void trySwitchContextOfLoadedModel(const ModelInfo &modelInfo); bool loadModel(const ModelInfo &modelInfo); @@ -210,22 +209,19 @@ public Q_SLOTS: void unloadModel(); void reloadModel(); void generateName(); - void generateQuestions(qint64 elapsed); void handleChatIdChanged(const QString &id); void handleShouldBeLoadedChanged(); void handleThreadStarted(); void handleForceMetalChanged(bool forceMetal); void handleDeviceChanged(); - void processSystemPrompt(); - void processRestoreStateFromText(); Q_SIGNALS: - void restoringFromTextChanged(); void loadedModelInfoChanged(); void modelLoadingPercentageChanged(float); void modelLoadingError(const QString &error); void modelLoadingWarning(const QString &warning); - void responseChanged(const QString &response); + void responseChanged(); + void responseFailed(); void promptProcessing(); void generatingQuestions(); void responseStopped(qint64 promptResponseMs); @@ -244,58 +240,51 @@ public Q_SLOTS: void modelInfoChanged(const ModelInfo &modelInfo); protected: - bool promptInternal(const QList &collectionList, const QString &prompt, const QString &promptTemplate, - int32_t n_predict, int32_t top_k, float top_p, float min_p, float temp, int32_t n_batch, float repeat_penalty, - int32_t repeat_penalty_tokens, std::optional fakeReply = {}); - bool handlePrompt(int32_t token); - bool handleResponse(int32_t token, const std::string &response); - bool handleNamePrompt(int32_t token); - bool handleNameResponse(int32_t token, const std::string &response); - bool handleSystemPrompt(int32_t token); - bool handleSystemResponse(int32_t token, const std::string &response); - bool handleRestoreStateFromTextPrompt(int32_t token); - bool handleRestoreStateFromTextResponse(int32_t token, const std::string &response); - bool handleQuestionPrompt(int32_t token); - bool handleQuestionResponse(int32_t token, const std::string &response); - void saveState(); - void restoreState(); - -protected: - LLModel::PromptContext m_ctx; - quint32 m_promptTokens; - quint32 m_promptResponseTokens; + struct PromptResult { + QByteArray response; // raw UTF-8 + int promptTokens; // note: counts *entire* history, even if cached + int responseTokens; + }; + + struct ChatPromptResult : PromptResult { + QList databaseResults; + }; + + ChatPromptResult promptInternalChat(const QStringList &enabledCollections, const LLModel::PromptContext &ctx, + qsizetype startOffset = 0); + // passing a string_view directly skips templating and uses the raw string + PromptResult promptInternal(const std::variant, std::string_view> &prompt, + const LLModel::PromptContext &ctx, + bool usedLocalDocs); private: bool loadNewModel(const ModelInfo &modelInfo, QVariantMap &modelLoadProps); + std::vector forkConversation(const QString &prompt) const; + + // Applies the Jinja template. Query mode returns only the last message without special tokens. + // Returns a (# of messages, rendered prompt) pair. + std::string applyJinjaTemplate(std::span items) const; + + void generateQuestions(qint64 elapsed); + +protected: + QPointer m_chatModel; + +private: const Chat *m_chat; - std::string m_response; - std::string m_trimmedResponse; - std::string m_nameResponse; - QString m_questionResponse; LLModelInfo m_llModelInfo; LLModelTypeV1 m_llModelType = LLModelTypeV1::NONE; ModelInfo m_modelInfo; TokenTimer *m_timer; - QByteArray m_state; - std::vector m_stateInputTokens; - int32_t m_stateContextLength = -1; QThread m_llmThread; std::atomic m_stopGenerating; std::atomic m_shouldBeLoaded; - std::atomic m_restoringFromText; // status indication std::atomic m_forceUnloadModel; std::atomic m_markedForDeletion; bool m_isServer; bool m_forceMetal; bool m_reloadingToChangeVariant; - bool m_processedSystemPrompt; - bool m_restoreStateFromText; - // m_pristineLoadedState is set if saveSate is unnecessary, either because: - // - an unload was queued during LLModel::restoreState() - // - the chat will be restored from text and hasn't been interacted with yet - bool m_pristineLoadedState = false; - QPointer m_chatModel; }; #endif // CHATLLM_H diff --git a/gpt4all-chat/src/chatmodel.cpp b/gpt4all-chat/src/chatmodel.cpp new file mode 100644 index 000000000000..f18bd1e17cad --- /dev/null +++ b/gpt4all-chat/src/chatmodel.cpp @@ -0,0 +1,366 @@ +#include "chatmodel.h" + +#include +#include +#include +#include + + +QList ChatItem::consolidateSources(const QList &sources) +{ + QMap groupedData; + for (const ResultInfo &info : sources) { + if (groupedData.contains(info.file)) { + groupedData[info.file].text += "\n---\n" + info.text; + } else { + groupedData[info.file] = info; + } + } + QList consolidatedSources = groupedData.values(); + return consolidatedSources; +} + +void ChatItem::serializeResponse(QDataStream &stream, int version) +{ + stream << value; +} + +void ChatItem::serializeToolCall(QDataStream &stream, int version) +{ + stream << value; + toolCallInfo.serialize(stream, version); +} + +void ChatItem::serializeToolResponse(QDataStream &stream, int version) +{ + stream << value; +} + +void ChatItem::serializeText(QDataStream &stream, int version) +{ + stream << value; +} + +void ChatItem::serializeThink(QDataStream &stream, int version) +{ + stream << value; + stream << thinkingTime; +} + +void ChatItem::serializeSubItems(QDataStream &stream, int version) +{ + stream << name; + switch (auto typ = type()) { + using enum ChatItem::Type; + case Response: { serializeResponse(stream, version); break; } + case ToolCall: { serializeToolCall(stream, version); break; } + case ToolResponse: { serializeToolResponse(stream, version); break; } + case Text: { serializeText(stream, version); break; } + case Think: { serializeThink(stream, version); break; } + case System: + case Prompt: + throw std::invalid_argument(fmt::format("cannot serialize subitem type {}", int(typ))); + } + + stream << qsizetype(subItems.size()); + for (ChatItem *item :subItems) + item->serializeSubItems(stream, version); +} + +void ChatItem::serialize(QDataStream &stream, int version) +{ + stream << name; + stream << value; + stream << newResponse; + stream << isCurrentResponse; + stream << stopped; + stream << thumbsUpState; + stream << thumbsDownState; + if (version >= 11 && type() == ChatItem::Type::Response) + stream << isError; + if (version >= 8) { + stream << sources.size(); + for (const ResultInfo &info : sources) { + Q_ASSERT(!info.file.isEmpty()); + stream << info.collection; + stream << info.path; + stream << info.file; + stream << info.title; + stream << info.author; + stream << info.date; + stream << info.text; + stream << info.page; + stream << info.from; + stream << info.to; + } + } else if (version >= 3) { + QList references; + QList referencesContext; + int validReferenceNumber = 1; + for (const ResultInfo &info : sources) { + if (info.file.isEmpty()) + continue; + + QString reference; + { + QTextStream stream(&reference); + stream << (validReferenceNumber++) << ". "; + if (!info.title.isEmpty()) + stream << "\"" << info.title << "\". "; + if (!info.author.isEmpty()) + stream << "By " << info.author << ". "; + if (!info.date.isEmpty()) + stream << "Date: " << info.date << ". "; + stream << "In " << info.file << ". "; + if (info.page != -1) + stream << "Page " << info.page << ". "; + if (info.from != -1) { + stream << "Lines " << info.from; + if (info.to != -1) + stream << "-" << info.to; + stream << ". "; + } + stream << "[Context](context://" << validReferenceNumber - 1 << ")"; + } + references.append(reference); + referencesContext.append(info.text); + } + + stream << references.join("\n"); + stream << referencesContext; + } + if (version >= 10) { + stream << promptAttachments.size(); + for (const PromptAttachment &a : promptAttachments) { + Q_ASSERT(!a.url.isEmpty()); + stream << a.url; + stream << a.content; + } + } + + if (version >= 12) { + stream << qsizetype(subItems.size()); + for (ChatItem *item :subItems) + item->serializeSubItems(stream, version); + } +} + +bool ChatItem::deserializeToolCall(QDataStream &stream, int version) +{ + stream >> value; + return toolCallInfo.deserialize(stream, version);; +} + +bool ChatItem::deserializeToolResponse(QDataStream &stream, int version) +{ + stream >> value; + return true; +} + +bool ChatItem::deserializeText(QDataStream &stream, int version) +{ + stream >> value; + return true; +} + +bool ChatItem::deserializeResponse(QDataStream &stream, int version) +{ + stream >> value; + return true; +} + +bool ChatItem::deserializeThink(QDataStream &stream, int version) +{ + stream >> value; + stream >> thinkingTime; + return true; +} + +bool ChatItem::deserializeSubItems(QDataStream &stream, int version) +{ + stream >> name; + try { + type(); // check name + } catch (const std::exception &e) { + qWarning() << "ChatModel ERROR:" << e.what(); + return false; + } + switch (auto typ = type()) { + using enum ChatItem::Type; + case Response: { deserializeResponse(stream, version); break; } + case ToolCall: { deserializeToolCall(stream, version); break; } + case ToolResponse: { deserializeToolResponse(stream, version); break; } + case Text: { deserializeText(stream, version); break; } + case Think: { deserializeThink(stream, version); break; } + case System: + case Prompt: + throw std::invalid_argument(fmt::format("cannot serialize subitem type {}", int(typ))); + } + + qsizetype count; + stream >> count; + for (int i = 0; i < count; ++i) { + ChatItem *c = new ChatItem(this); + if (!c->deserializeSubItems(stream, version)) { + delete c; + return false; + } + subItems.push_back(c); + } + + return true; +} + +bool ChatItem::deserialize(QDataStream &stream, int version) +{ + if (version < 12) { + int id; + stream >> id; + } + stream >> name; + try { + type(); // check name + } catch (const std::exception &e) { + qWarning() << "ChatModel ERROR:" << e.what(); + return false; + } + stream >> value; + if (version < 10) { + // This is deprecated and no longer used + QString prompt; + stream >> prompt; + } + stream >> newResponse; + stream >> isCurrentResponse; + stream >> stopped; + stream >> thumbsUpState; + stream >> thumbsDownState; + if (version >= 11 && type() == ChatItem::Type::Response) + stream >> isError; + if (version >= 8) { + qsizetype count; + stream >> count; + for (int i = 0; i < count; ++i) { + ResultInfo info; + stream >> info.collection; + stream >> info.path; + stream >> info.file; + stream >> info.title; + stream >> info.author; + stream >> info.date; + stream >> info.text; + stream >> info.page; + stream >> info.from; + stream >> info.to; + sources.append(info); + } + consolidatedSources = ChatItem::consolidateSources(sources); + } else if (version >= 3) { + QString references; + QList referencesContext; + stream >> references; + stream >> referencesContext; + + if (!references.isEmpty()) { + QList referenceList = references.split("\n"); + + // Ignore empty lines and those that begin with "---" which is no longer used + for (auto it = referenceList.begin(); it != referenceList.end();) { + if (it->trimmed().isEmpty() || it->trimmed().startsWith("---")) + it = referenceList.erase(it); + else + ++it; + } + + Q_ASSERT(referenceList.size() == referencesContext.size()); + for (int j = 0; j < referenceList.size(); ++j) { + QString reference = referenceList[j]; + QString context = referencesContext[j]; + ResultInfo info; + QTextStream refStream(&reference); + QString dummy; + int validReferenceNumber; + refStream >> validReferenceNumber >> dummy; + // Extract title (between quotes) + if (reference.contains("\"")) { + int startIndex = reference.indexOf('"') + 1; + int endIndex = reference.indexOf('"', startIndex); + info.title = reference.mid(startIndex, endIndex - startIndex); + } + + // Extract author (after "By " and before the next period) + if (reference.contains("By ")) { + int startIndex = reference.indexOf("By ") + 3; + int endIndex = reference.indexOf('.', startIndex); + info.author = reference.mid(startIndex, endIndex - startIndex).trimmed(); + } + + // Extract date (after "Date: " and before the next period) + if (reference.contains("Date: ")) { + int startIndex = reference.indexOf("Date: ") + 6; + int endIndex = reference.indexOf('.', startIndex); + info.date = reference.mid(startIndex, endIndex - startIndex).trimmed(); + } + + // Extract file name (after "In " and before the "[Context]") + if (reference.contains("In ") && reference.contains(". [Context]")) { + int startIndex = reference.indexOf("In ") + 3; + int endIndex = reference.indexOf(". [Context]", startIndex); + info.file = reference.mid(startIndex, endIndex - startIndex).trimmed(); + } + + // Extract page number (after "Page " and before the next space) + if (reference.contains("Page ")) { + int startIndex = reference.indexOf("Page ") + 5; + int endIndex = reference.indexOf(' ', startIndex); + if (endIndex == -1) endIndex = reference.length(); + info.page = reference.mid(startIndex, endIndex - startIndex).toInt(); + } + + // Extract lines (after "Lines " and before the next space or hyphen) + if (reference.contains("Lines ")) { + int startIndex = reference.indexOf("Lines ") + 6; + int endIndex = reference.indexOf(' ', startIndex); + if (endIndex == -1) endIndex = reference.length(); + int hyphenIndex = reference.indexOf('-', startIndex); + if (hyphenIndex != -1 && hyphenIndex < endIndex) { + info.from = reference.mid(startIndex, hyphenIndex - startIndex).toInt(); + info.to = reference.mid(hyphenIndex + 1, endIndex - hyphenIndex - 1).toInt(); + } else { + info.from = reference.mid(startIndex, endIndex - startIndex).toInt(); + } + } + info.text = context; + sources.append(info); + } + + consolidatedSources = ChatItem::consolidateSources(sources); + } + } + if (version >= 10) { + qsizetype count; + stream >> count; + QList attachments; + for (int i = 0; i < count; ++i) { + PromptAttachment a; + stream >> a.url; + stream >> a.content; + attachments.append(a); + } + promptAttachments = attachments; + } + + if (version >= 12) { + qsizetype count; + stream >> count; + for (int i = 0; i < count; ++i) { + ChatItem *c = new ChatItem(this); + if (!c->deserializeSubItems(stream, version)) { + delete c; + return false; + } + subItems.push_back(c); + } + } + return true; +} diff --git a/gpt4all-chat/src/chatmodel.h b/gpt4all-chat/src/chatmodel.h index 5a5c63b2d6ee..a340b0ca804f 100644 --- a/gpt4all-chat/src/chatmodel.h +++ b/gpt4all-chat/src/chatmodel.h @@ -2,12 +2,20 @@ #define CHATMODEL_H #include "database.h" +#include "tool.h" +#include "toolcallparser.h" +#include "utils.h" #include "xlsxtomd.h" +#include + +#include #include #include #include +#include #include +#include #include #include #include @@ -18,6 +26,18 @@ #include #include +#include +#include +#include +#include +#include +#include + +using namespace Qt::Literals::StringLiterals; +namespace ranges = std::ranges; +namespace views = std::views; + + struct PromptAttachment { Q_GADGET Q_PROPERTY(QUrl url MEMBER url) @@ -57,69 +77,449 @@ struct PromptAttachment { }; Q_DECLARE_METATYPE(PromptAttachment) -struct ChatItem +// Used by Server to represent a message from the client. +struct MessageInput +{ + enum class Type { System, Prompt, Response }; + Type type; + QString content; +}; + +class MessageItem { Q_GADGET - Q_PROPERTY(QString name MEMBER name) + Q_PROPERTY(Type type READ type CONSTANT) + Q_PROPERTY(QString content READ content CONSTANT) + +public: + enum class Type { System, Prompt, Response, ToolResponse }; + + struct system_tag_t { explicit system_tag_t() = default; }; + static inline constexpr system_tag_t system_tag = system_tag_t{}; + + MessageItem(qsizetype index, Type type, QString content) + : m_index(index), m_type(type), m_content(std::move(content)) + { + Q_ASSERT(type != Type::System); // use system_tag constructor + } + + // Construct a system message with no index, since they are never stored in the chat + MessageItem(system_tag_t, QString content) + : m_type(Type::System), m_content(std::move(content)) {} + + MessageItem(qsizetype index, Type type, QString content, const QList &sources, const QList &promptAttachments) + : m_index(index) + , m_type(type) + , m_content(std::move(content)) + , m_sources(sources) + , m_promptAttachments(promptAttachments) {} + + // index of the parent ChatItem (system, prompt, response) in its container + std::optional index() const { return m_index; } + + Type type() const { return m_type; } + const QString &content() const { return m_content; } + + QList sources() const { return m_sources; } + QList promptAttachments() const { return m_promptAttachments; } + + // used with version 0 Jinja templates + QString bakedPrompt() const + { + if (type() != Type::Prompt) + throw std::logic_error("bakedPrompt() called on non-prompt item"); + QStringList parts; + if (!m_sources.isEmpty()) { + parts << u"### Context:\n"_s; + for (auto &source : std::as_const(m_sources)) + parts << u"Collection: "_s << source.collection + << u"\nPath: "_s << source.path + << u"\nExcerpt: "_s << source.text << u"\n\n"_s; + } + for (auto &attached : std::as_const(m_promptAttachments)) + parts << attached.processedContent() << u"\n\n"_s; + parts << m_content; + return parts.join(QString()); + } + +private: + std::optional m_index; + Type m_type; + QString m_content; + QList m_sources; + QList m_promptAttachments; +}; +Q_DECLARE_METATYPE(MessageItem) + +class ChatItem : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name MEMBER name ) Q_PROPERTY(QString value MEMBER value) - Q_PROPERTY(QString newResponse MEMBER newResponse) - Q_PROPERTY(bool currentResponse MEMBER currentResponse) - Q_PROPERTY(bool stopped MEMBER stopped) - Q_PROPERTY(bool thumbsUpState MEMBER thumbsUpState) - Q_PROPERTY(bool thumbsDownState MEMBER thumbsDownState) - Q_PROPERTY(QList sources MEMBER sources) - Q_PROPERTY(QList consolidatedSources MEMBER consolidatedSources) + + // prompts and responses + Q_PROPERTY(QString content READ content NOTIFY contentChanged) + + // prompts Q_PROPERTY(QList promptAttachments MEMBER promptAttachments) - Q_PROPERTY(QString promptPlusAttachments READ promptPlusAttachments) + + // responses + Q_PROPERTY(bool isCurrentResponse MEMBER isCurrentResponse NOTIFY isCurrentResponseChanged) + Q_PROPERTY(bool isError MEMBER isError ) + Q_PROPERTY(QList childItems READ childItems ) + + // toolcall + Q_PROPERTY(bool isToolCallError READ isToolCallError NOTIFY isTooCallErrorChanged) + + // responses (DataLake) + Q_PROPERTY(QString newResponse MEMBER newResponse ) + Q_PROPERTY(bool stopped MEMBER stopped ) + Q_PROPERTY(bool thumbsUpState MEMBER thumbsUpState ) + Q_PROPERTY(bool thumbsDownState MEMBER thumbsDownState) + + // thinking + Q_PROPERTY(int thinkingTime MEMBER thinkingTime NOTIFY thinkingTimeChanged) + +public: + enum class Type { System, Prompt, Response, Text, ToolCall, ToolResponse, Think }; + + // tags for constructing ChatItems + struct prompt_tag_t { explicit prompt_tag_t () = default; }; + struct response_tag_t { explicit response_tag_t () = default; }; + struct system_tag_t { explicit system_tag_t () = default; }; + struct text_tag_t { explicit text_tag_t () = default; }; + struct tool_call_tag_t { explicit tool_call_tag_t () = default; }; + struct tool_response_tag_t { explicit tool_response_tag_t() = default; }; + struct think_tag_t { explicit think_tag_t () = default; }; + static inline constexpr prompt_tag_t prompt_tag = prompt_tag_t {}; + static inline constexpr response_tag_t response_tag = response_tag_t {}; + static inline constexpr system_tag_t system_tag = system_tag_t {}; + static inline constexpr text_tag_t text_tag = text_tag_t {}; + static inline constexpr tool_call_tag_t tool_call_tag = tool_call_tag_t {}; + static inline constexpr tool_response_tag_t tool_response_tag = tool_response_tag_t {}; + static inline constexpr think_tag_t think_tag = think_tag_t {}; + +public: + ChatItem(QObject *parent) + : QObject(nullptr) + { + moveToThread(parent->thread()); + setParent(parent); + } + + // NOTE: System messages are currently never serialized and only *stored* by the local server. + // ChatLLM prepends a system MessageItem on-the-fly. + ChatItem(QObject *parent, system_tag_t, const QString &value) + : ChatItem(parent) + { this->name = u"System: "_s; this->value = value; } + + ChatItem(QObject *parent, prompt_tag_t, const QString &value, const QList &attachments = {}) + : ChatItem(parent) + { this->name = u"Prompt: "_s; this->value = value; this->promptAttachments = attachments; } + +private: + ChatItem(QObject *parent, response_tag_t, bool isCurrentResponse, const QString &value = {}) + : ChatItem(parent) + { this->name = u"Response: "_s; this->value = value; this->isCurrentResponse = isCurrentResponse; } public: - QString promptPlusAttachments() const + // A new response, to be filled in + ChatItem(QObject *parent, response_tag_t) + : ChatItem(parent, response_tag, true) {} + + // An existing response, from Server + ChatItem(QObject *parent, response_tag_t, const QString &value) + : ChatItem(parent, response_tag, false, value) {} + + ChatItem(QObject *parent, text_tag_t, const QString &value) + : ChatItem(parent) + { this->name = u"Text: "_s; this->value = value; } + + ChatItem(QObject *parent, tool_call_tag_t, const QString &value) + : ChatItem(parent) + { this->name = u"ToolCall: "_s; this->value = value; } + + ChatItem(QObject *parent, tool_response_tag_t, const QString &value) + : ChatItem(parent) + { this->name = u"ToolResponse: "_s; this->value = value; } + + ChatItem(QObject *parent, think_tag_t, const QString &value) + : ChatItem(parent) + { this->name = u"Think: "_s; this->value = value; } + + Type type() const + { + if (name == u"System: "_s) + return Type::System; + if (name == u"Prompt: "_s) + return Type::Prompt; + if (name == u"Response: "_s) + return Type::Response; + if (name == u"Text: "_s) + return Type::Text; + if (name == u"ToolCall: "_s) + return Type::ToolCall; + if (name == u"ToolResponse: "_s) + return Type::ToolResponse; + if (name == u"Think: "_s) + return Type::Think; + throw std::invalid_argument(fmt::format("Chat item has unknown label: {:?}", name)); + } + + QString flattenedContent() const + { + if (subItems.empty()) + return value; + + // We only flatten one level + QString content; + for (ChatItem *item : subItems) + content += item->value; + return content; + } + + QString content() const + { + if (type() == Type::Response) { + // We parse if this contains any part of a partial toolcall + ToolCallParser parser; + parser.update(value.toUtf8()); + + // If no tool call is detected, return the original value + if (parser.startIndex() < 0) + return value; + + // Otherwise we only return the text before and any partial tool call + const QString beforeToolCall = value.left(parser.startIndex()); + return beforeToolCall; + } + + if (type() == Type::Think) + return thinkContent(value); + + if (type() == Type::ToolCall) + return toolCallContent(value); + + // We don't show any of content from the tool response in the GUI + if (type() == Type::ToolResponse) + return QString(); + + return value; + } + + QString thinkContent(const QString &value) const + { + ToolCallParser parser; + parser.update(value.toUtf8()); + + // Extract the content + QString content = parser.toolCall(); + content = content.trimmed(); + return content; + } + + QString toolCallContent(const QString &value) const + { + ToolCallParser parser; + parser.update(value.toUtf8()); + + // Extract the code + QString code = parser.toolCall(); + code = code.trimmed(); + + QString result; + + // If we've finished the tool call then extract the result from meta information + if (toolCallInfo.name == ToolCallConstants::CodeInterpreterFunction) + result = "```\n" + toolCallInfo.result + "```"; + + // Return the formatted code and the result if available + return code + result; + } + + QString clipboardContent() const + { + QStringList clipContent; + for (const ChatItem *item : subItems) + clipContent << item->clipboardContent(); + clipContent << content(); + return clipContent.join(""); + } + + QList childItems() const { - QStringList attachedContexts; - for (auto attached : promptAttachments) - attachedContexts << attached.processedContent(); + // We currently have leaf nodes at depth 3 with nodes at depth 2 as mere containers we don't + // care about in GUI + QList items; + for (const ChatItem *item : subItems) { + items.reserve(items.size() + item->subItems.size()); + ranges::copy(item->subItems, std::back_inserter(items)); + } + return items; + } - QString promptPlus = value; - if (!attachedContexts.isEmpty()) - promptPlus = attachedContexts.join("\n\n") + "\n\n" + value; - return promptPlus; + QString possibleToolCall() const + { + if (!subItems.empty()) + return subItems.back()->possibleToolCall(); + if (type() == Type::ToolCall) + return value; + else + return QString(); } + void setCurrentResponse(bool b) + { + if (!subItems.empty()) + subItems.back()->setCurrentResponse(b); + isCurrentResponse = b; + emit isCurrentResponseChanged(); + } + + void setValue(const QString &v) + { + if (!subItems.empty() && subItems.back()->isCurrentResponse) { + subItems.back()->setValue(v); + return; + } + + value = v; + emit contentChanged(); + } + + void setToolCallInfo(const ToolCallInfo &info) + { + toolCallInfo = info; + emit contentChanged(); + emit isTooCallErrorChanged(); + } + + bool isToolCallError() const + { + return toolCallInfo.error != ToolEnums::Error::NoError; + } + + void setThinkingTime(int t) + { + thinkingTime = t; + emit thinkingTimeChanged(); + } + + // NB: Assumes response is not current. + static ChatItem *fromMessageInput(QObject *parent, const MessageInput &message) + { + switch (message.type) { + using enum MessageInput::Type; + case Prompt: return new ChatItem(parent, prompt_tag, message.content); + case Response: return new ChatItem(parent, response_tag, message.content); + case System: return new ChatItem(parent, system_tag, message.content); + } + Q_UNREACHABLE(); + } + + MessageItem asMessageItem(qsizetype index) const + { + MessageItem::Type msgType; + switch (auto typ = type()) { + using enum ChatItem::Type; + case System: msgType = MessageItem::Type::System; break; + case Prompt: msgType = MessageItem::Type::Prompt; break; + case Response: msgType = MessageItem::Type::Response; break; + case ToolResponse: msgType = MessageItem::Type::ToolResponse; break; + case Text: + case ToolCall: + case Think: + throw std::invalid_argument(fmt::format("cannot convert ChatItem type {} to message item", int(typ))); + } + return { index, msgType, flattenedContent(), sources, promptAttachments }; + } + + static QList consolidateSources(const QList &sources); + + void serializeResponse(QDataStream &stream, int version); + void serializeToolCall(QDataStream &stream, int version); + void serializeToolResponse(QDataStream &stream, int version); + void serializeText(QDataStream &stream, int version); + void serializeThink(QDataStream &stream, int version); + void serializeSubItems(QDataStream &stream, int version); // recursive + void serialize(QDataStream &stream, int version); + + + bool deserializeResponse(QDataStream &stream, int version); + bool deserializeToolCall(QDataStream &stream, int version); + bool deserializeToolResponse(QDataStream &stream, int version); + bool deserializeText(QDataStream &stream, int version); + bool deserializeThink(QDataStream &stream, int version); + bool deserializeSubItems(QDataStream &stream, int version); // recursive + bool deserialize(QDataStream &stream, int version); + +Q_SIGNALS: + void contentChanged(); + void isTooCallErrorChanged(); + void isCurrentResponseChanged(); + void thinkingTimeChanged(); + +public: + // TODO: Maybe we should include the model name here as well as timestamp? QString name; QString value; - QString newResponse; - QList sources; - QList consolidatedSources; + + // prompts + QList sources; + QList consolidatedSources; QList promptAttachments; - bool currentResponse = false; - bool stopped = false; - bool thumbsUpState = false; - bool thumbsDownState = false; -}; -Q_DECLARE_METATYPE(ChatItem) -using ChatModelIterator = QList::const_iterator; + // responses + bool isCurrentResponse = false; + bool isError = false; + ToolCallInfo toolCallInfo; + std::list subItems; + + // responses (DataLake) + QString newResponse; + bool stopped = false; + bool thumbsUpState = false; + bool thumbsDownState = false; + + // thinking time in ms + int thinkingTime = 0; +}; class ChatModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(bool hasError READ hasError NOTIFY hasErrorChanged) public: - explicit ChatModel(QObject *parent = nullptr) : QAbstractListModel(parent) {} + explicit ChatModel(QObject *parent = nullptr) + : QAbstractListModel(parent) {} + // FIXME(jared): can't this start at Qt::UserRole (no +1)? enum Roles { NameRole = Qt::UserRole + 1, ValueRole, + + // prompts and responses + ContentRole, + + // prompts + PromptAttachmentsRole, + + // responses + // NOTE: sources are stored on the *prompts*, but in the model, they are only on the *responses*! + SourcesRole, + ConsolidatedSourcesRole, + IsCurrentResponseRole, + IsErrorRole, + ChildItemsRole, + + // responses (DataLake) NewResponseRole, - CurrentResponseRole, StoppedRole, ThumbsUpStateRole, ThumbsDownStateRole, - SourcesRole, - ConsolidatedSourcesRole, - PromptAttachmentsRole }; int rowCount(const QModelIndex &parent = QModelIndex()) const override @@ -129,34 +529,112 @@ class ChatModel : public QAbstractListModel return m_chatItems.size(); } + /* a "peer" is a bidirectional 1:1 link between a prompt and the response that would cite its LocalDocs + * sources. Return std::nullopt if there is none, which is possible for e.g. server chats. */ + template + static std::optional getPeer(const T *arr, qsizetype size, qsizetype index) + { + Q_ASSERT(index >= 0); + Q_ASSERT(index < size); + return getPeerInternal(arr, size, index); + } + +private: + static std::optional getPeerInternal(const ChatItem * const *arr, qsizetype size, qsizetype index) + { + qsizetype peer; + ChatItem::Type expected; + switch (arr[index]->type()) { + using enum ChatItem::Type; + case Prompt: peer = index + 1; expected = Response; break; + case Response: peer = index - 1; expected = Prompt; break; + default: throw std::invalid_argument("getPeer() called on item that is not a prompt or response"); + } + if (peer >= 0 && peer < size && arr[peer]->type() == expected) + return peer; + return std::nullopt; + } + + // FIXME(jared): this should really be done at the parent level, not the sub-item level + static std::optional getPeerInternal(const MessageItem *arr, qsizetype size, qsizetype index) + { + qsizetype peer; + MessageItem::Type expected; + switch (arr[index].type()) { + using enum MessageItem::Type; + case Prompt: peer = index + 1; expected = Response; break; + case Response: peer = index - 1; expected = Prompt; break; + default: throw std::invalid_argument("getPeer() called on item that is not a prompt or response"); + } + if (peer >= 0 && peer < size && arr[peer].type() == expected) + return peer; + return std::nullopt; + } + +public: + template + static auto getPeer(R &&range, ranges::iterator_t item) -> std::optional> + { + auto begin = ranges::begin(range); + return getPeer(ranges::data(range), ranges::size(range), item - begin) + .transform([&](auto i) { return begin + i; }); + } + + auto getPeerUnlocked(QList::const_iterator item) const -> std::optional::const_iterator> + { return getPeer(m_chatItems, item); } + + std::optional getPeerUnlocked(qsizetype index) const + { return getPeer(m_chatItems.constData(), m_chatItems.size(), index); } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override { QMutexLocker locker(&m_mutex); if (!index.isValid() || index.row() < 0 || index.row() >= m_chatItems.size()) return QVariant(); - const ChatItem &item = m_chatItems.at(index.row()); + auto itemIt = m_chatItems.cbegin() + index.row(); + auto *item = *itemIt; switch (role) { case NameRole: - return item.name; + return item->name; case ValueRole: - return item.value; + return item->value; + case PromptAttachmentsRole: + return QVariant::fromValue(item->promptAttachments); + case SourcesRole: + { + QList data; + if (item->type() == ChatItem::Type::Response) { + if (auto prompt = getPeerUnlocked(itemIt)) + data = (**prompt)->sources; + } + return QVariant::fromValue(data); + } + case ConsolidatedSourcesRole: + { + QList data; + if (item->type() == ChatItem::Type::Response) { + if (auto prompt = getPeerUnlocked(itemIt)) + data = (**prompt)->consolidatedSources; + } + return QVariant::fromValue(data); + } + case IsCurrentResponseRole: + return item->isCurrentResponse; case NewResponseRole: - return item.newResponse; - case CurrentResponseRole: - return item.currentResponse; + return item->newResponse; case StoppedRole: - return item.stopped; + return item->stopped; case ThumbsUpStateRole: - return item.thumbsUpState; + return item->thumbsUpState; case ThumbsDownStateRole: - return item.thumbsDownState; - case SourcesRole: - return QVariant::fromValue(item.sources); - case ConsolidatedSourcesRole: - return QVariant::fromValue(item.consolidatedSources); - case PromptAttachmentsRole: - return QVariant::fromValue(item.promptAttachments); + return item->thumbsDownState; + case IsErrorRole: + return item->type() == ChatItem::Type::Response && item->isError; + case ContentRole: + return item->content(); + case ChildItemsRole: + return QVariant::fromValue(item->childItems()); } return QVariant(); @@ -164,54 +642,157 @@ class ChatModel : public QAbstractListModel QHash roleNames() const override { - QHash roles; - roles[NameRole] = "name"; - roles[ValueRole] = "value"; - roles[NewResponseRole] = "newResponse"; - roles[CurrentResponseRole] = "currentResponse"; - roles[StoppedRole] = "stopped"; - roles[ThumbsUpStateRole] = "thumbsUpState"; - roles[ThumbsDownStateRole] = "thumbsDownState"; - roles[SourcesRole] = "sources"; - roles[ConsolidatedSourcesRole] = "consolidatedSources"; - roles[PromptAttachmentsRole] = "promptAttachments"; - return roles; + return { + { NameRole, "name" }, + { ValueRole, "value" }, + { PromptAttachmentsRole, "promptAttachments" }, + { SourcesRole, "sources" }, + { ConsolidatedSourcesRole, "consolidatedSources" }, + { IsCurrentResponseRole, "isCurrentResponse" }, + { IsErrorRole, "isError" }, + { NewResponseRole, "newResponse" }, + { StoppedRole, "stopped" }, + { ThumbsUpStateRole, "thumbsUpState" }, + { ThumbsDownStateRole, "thumbsDownState" }, + { ContentRole, "content" }, + { ChildItemsRole, "childItems" }, + }; } - void appendPrompt(const QString &name, const QString &value, const QList &attachments) + void appendPrompt(const QString &value, const QList &attachments = {}) { - ChatItem item; - item.name = name; - item.value = value; - item.promptAttachments << attachments; + qsizetype count; + { + QMutexLocker locker(&m_mutex); + if (hasErrorUnlocked()) + throw std::logic_error("cannot append to a failed chat"); + count = m_chatItems.count(); + } - m_mutex.lock(); - const int count = m_chatItems.count(); - m_mutex.unlock(); beginInsertRows(QModelIndex(), count, count); { QMutexLocker locker(&m_mutex); - m_chatItems.append(item); + m_chatItems << new ChatItem(this, ChatItem::prompt_tag, value, attachments); } endInsertRows(); emit countChanged(); } - void appendResponse(const QString &name) + void appendResponse() { + qsizetype count; + { + QMutexLocker locker(&m_mutex); + if (hasErrorUnlocked()) + throw std::logic_error("cannot append to a failed chat"); + count = m_chatItems.count(); + } + + beginInsertRows(QModelIndex(), count, count); + { + QMutexLocker locker(&m_mutex); + m_chatItems << new ChatItem(this, ChatItem::response_tag); + } + endInsertRows(); + emit countChanged(); + } + + // Used by Server to append a new conversation to the chat log. + // Returns the offset of the appended items. + qsizetype appendResponseWithHistory(std::span history) + { + if (history.empty()) + throw std::invalid_argument("at least one message is required"); + m_mutex.lock(); - const int count = m_chatItems.count(); + qsizetype startIndex = m_chatItems.size(); m_mutex.unlock(); - ChatItem item; - item.name = name; - item.currentResponse = true; - beginInsertRows(QModelIndex(), count, count); + + qsizetype nNewItems = history.size() + 1; + qsizetype endIndex = startIndex + nNewItems; + beginInsertRows(QModelIndex(), startIndex, endIndex - 1 /*inclusive*/); + bool hadError; + QList newItems; { QMutexLocker locker(&m_mutex); - m_chatItems.append(item); + startIndex = m_chatItems.size(); // just in case + hadError = hasErrorUnlocked(); + m_chatItems.reserve(m_chatItems.count() + nNewItems); + for (auto &message : history) + m_chatItems << ChatItem::fromMessageInput(this, message); + m_chatItems << new ChatItem(this, ChatItem::response_tag); } endInsertRows(); emit countChanged(); + // Server can add messages when there is an error because each call is a new conversation + if (hadError) + emit hasErrorChanged(false); + return startIndex; + } + + void truncate(qsizetype size) + { + qsizetype oldSize; + { + QMutexLocker locker(&m_mutex); + if (size >= (oldSize = m_chatItems.size())) + return; + if (size && m_chatItems.at(size - 1)->type() != ChatItem::Type::Response) + throw std::invalid_argument( + fmt::format("chat model truncated to {} items would not end in a response", size) + ); + } + + bool oldHasError; + beginRemoveRows(QModelIndex(), size, oldSize - 1 /*inclusive*/); + { + QMutexLocker locker(&m_mutex); + oldHasError = hasErrorUnlocked(); + Q_ASSERT(size < m_chatItems.size()); + m_chatItems.resize(size); + } + endRemoveRows(); + emit countChanged(); + if (oldHasError) + emit hasErrorChanged(false); + } + + QString popPrompt(int index) + { + QString content; + { + QMutexLocker locker(&m_mutex); + if (index < 0 || index >= m_chatItems.size() || m_chatItems[index]->type() != ChatItem::Type::Prompt) + throw std::logic_error("attempt to pop a prompt, but this is not a prompt"); + content = m_chatItems[index]->content(); + } + truncate(index); + return content; + } + + bool regenerateResponse(int index) + { + int promptIdx; + { + QMutexLocker locker(&m_mutex); + auto items = m_chatItems; // holds lock + if (index < 1 || index >= items.size() || items[index]->type() != ChatItem::Type::Response) + return false; + promptIdx = getPeerUnlocked(index).value_or(-1); + } + + truncate(index + 1); + clearSubItems(index); + setResponseValue({}); + updateCurrentResponse(index, true ); + updateNewResponse (index, {} ); + updateStopped (index, false); + updateThumbsUpState (index, false); + updateThumbsDownState(index, false); + setError(false); + if (promptIdx >= 0) + updateSources(promptIdx, {}); + return true; } Q_INVOKABLE void clear() @@ -221,37 +802,37 @@ class ChatModel : public QAbstractListModel if (m_chatItems.isEmpty()) return; } + bool oldHasError; beginResetModel(); { QMutexLocker locker(&m_mutex); + oldHasError = hasErrorUnlocked(); m_chatItems.clear(); } endResetModel(); emit countChanged(); + if (oldHasError) + emit hasErrorChanged(false); } - Q_INVOKABLE ChatItem get(int index) + Q_INVOKABLE QString possibleToolcall() const { QMutexLocker locker(&m_mutex); - if (index < 0 || index >= m_chatItems.size()) return ChatItem(); - return m_chatItems.at(index); + if (m_chatItems.empty()) return QString(); + return m_chatItems.back()->possibleToolCall(); } Q_INVOKABLE void updateCurrentResponse(int index, bool b) { - bool changed = false; { QMutexLocker locker(&m_mutex); if (index < 0 || index >= m_chatItems.size()) return; - ChatItem &item = m_chatItems[index]; - if (item.currentResponse != b) { - item.currentResponse = b; - changed = true; - } + ChatItem *item = m_chatItems[index]; + item->setCurrentResponse(b); } - if (changed) emit dataChanged(createIndex(index, 0), createIndex(index, 0), {CurrentResponseRole}); + emit dataChanged(createIndex(index, 0), createIndex(index, 0), {IsCurrentResponseRole}); } Q_INVOKABLE void updateStopped(int index, bool b) @@ -261,59 +842,49 @@ class ChatModel : public QAbstractListModel QMutexLocker locker(&m_mutex); if (index < 0 || index >= m_chatItems.size()) return; - ChatItem &item = m_chatItems[index]; - if (item.stopped != b) { - item.stopped = b; + ChatItem *item = m_chatItems[index]; + if (item->stopped != b) { + item->stopped = b; changed = true; } } if (changed) emit dataChanged(createIndex(index, 0), createIndex(index, 0), {StoppedRole}); } - Q_INVOKABLE void updateValue(int index, const QString &value) + Q_INVOKABLE void setResponseValue(const QString &value) { - bool changed = false; + qsizetype index; { QMutexLocker locker(&m_mutex); - if (index < 0 || index >= m_chatItems.size()) return; + if (m_chatItems.isEmpty() || m_chatItems.cend()[-1]->type() != ChatItem::Type::Response) + throw std::logic_error("we only set this on a response"); - ChatItem &item = m_chatItems[index]; - if (item.value != value) { - item.value = value; - changed = true; - } + index = m_chatItems.count() - 1; + ChatItem *item = m_chatItems.back(); + item->setValue(value); } - if (changed) { - emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ValueRole}); - emit valueChanged(index, value); - } - } - - static QList consolidateSources(const QList &sources) { - QMap groupedData; - for (const ResultInfo &info : sources) { - if (groupedData.contains(info.file)) { - groupedData[info.file].text += "\n---\n" + info.text; - } else { - groupedData[info.file] = info; - } - } - QList consolidatedSources = groupedData.values(); - return consolidatedSources; + emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ValueRole, ContentRole}); } Q_INVOKABLE void updateSources(int index, const QList &sources) { + int responseIndex = -1; { QMutexLocker locker(&m_mutex); if (index < 0 || index >= m_chatItems.size()) return; - ChatItem &item = m_chatItems[index]; - item.sources = sources; - item.consolidatedSources = consolidateSources(sources); + auto promptItem = m_chatItems.begin() + index; + if ((*promptItem)->type() != ChatItem::Type::Prompt) + throw std::invalid_argument(fmt::format("item at index {} is not a prompt", index)); + if (auto peer = getPeerUnlocked(promptItem)) + responseIndex = *peer - m_chatItems.cbegin(); + (*promptItem)->sources = sources; + (*promptItem)->consolidatedSources = ChatItem::consolidateSources(sources); + } + if (responseIndex >= 0) { + emit dataChanged(createIndex(responseIndex, 0), createIndex(responseIndex, 0), {SourcesRole}); + emit dataChanged(createIndex(responseIndex, 0), createIndex(responseIndex, 0), {ConsolidatedSourcesRole}); } - emit dataChanged(createIndex(index, 0), createIndex(index, 0), {SourcesRole}); - emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ConsolidatedSourcesRole}); } Q_INVOKABLE void updateThumbsUpState(int index, bool b) @@ -323,9 +894,9 @@ class ChatModel : public QAbstractListModel QMutexLocker locker(&m_mutex); if (index < 0 || index >= m_chatItems.size()) return; - ChatItem &item = m_chatItems[index]; - if (item.thumbsUpState != b) { - item.thumbsUpState = b; + ChatItem *item = m_chatItems[index]; + if (item->thumbsUpState != b) { + item->thumbsUpState = b; changed = true; } } @@ -339,9 +910,9 @@ class ChatModel : public QAbstractListModel QMutexLocker locker(&m_mutex); if (index < 0 || index >= m_chatItems.size()) return; - ChatItem &item = m_chatItems[index]; - if (item.thumbsDownState != b) { - item.thumbsDownState = b; + ChatItem *item = m_chatItems[index]; + if (item->thumbsDownState != b) { + item->thumbsDownState = b; changed = true; } } @@ -355,259 +926,319 @@ class ChatModel : public QAbstractListModel QMutexLocker locker(&m_mutex); if (index < 0 || index >= m_chatItems.size()) return; - ChatItem &item = m_chatItems[index]; - if (item.newResponse != newResponse) { - item.newResponse = newResponse; + ChatItem *item = m_chatItems[index]; + if (item->newResponse != newResponse) { + item->newResponse = newResponse; changed = true; } } if (changed) emit dataChanged(createIndex(index, 0), createIndex(index, 0), {NewResponseRole}); } - int count() const { QMutexLocker locker(&m_mutex); return m_chatItems.size(); } + Q_INVOKABLE void splitThinking(const QPair &split) + { + qsizetype index; + { + QMutexLocker locker(&m_mutex); + if (m_chatItems.isEmpty() || m_chatItems.cend()[-1]->type() != ChatItem::Type::Response) + throw std::logic_error("can only set thinking on a chat that ends with a response"); + + index = m_chatItems.count() - 1; + ChatItem *currentResponse = m_chatItems.back(); + Q_ASSERT(currentResponse->isCurrentResponse); - ChatModelIterator begin() const { return m_chatItems.begin(); } - ChatModelIterator end() const { return m_chatItems.end(); } - void lock() { m_mutex.lock(); } - void unlock() { m_mutex.unlock(); } + // Create a new response container for any text and the thinking + ChatItem *newResponse = new ChatItem(this, ChatItem::response_tag); + + // Add preceding text if any + if (!split.first.isEmpty()) { + ChatItem *textItem = new ChatItem(this, ChatItem::text_tag, split.first); + newResponse->subItems.push_back(textItem); + } + + // Add the thinking item + Q_ASSERT(!split.second.isEmpty()); + ChatItem *thinkingItem = new ChatItem(this, ChatItem::think_tag, split.second); + thinkingItem->isCurrentResponse = true; + newResponse->subItems.push_back(thinkingItem); + + // Add new response and reset our value + currentResponse->subItems.push_back(newResponse); + currentResponse->value = QString(); + } + + emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ChildItemsRole, ContentRole}); + } + + Q_INVOKABLE void endThinking(const QPair &split, int thinkingTime) + { + qsizetype index; + { + QMutexLocker locker(&m_mutex); + if (m_chatItems.isEmpty() || m_chatItems.cend()[-1]->type() != ChatItem::Type::Response) + throw std::logic_error("can only end thinking on a chat that ends with a response"); + + index = m_chatItems.count() - 1; + ChatItem *currentResponse = m_chatItems.back(); + Q_ASSERT(currentResponse->isCurrentResponse); + + ChatItem *subResponse = currentResponse->subItems.back(); + Q_ASSERT(subResponse->type() == ChatItem::Type::Response); + Q_ASSERT(subResponse->isCurrentResponse); + subResponse->setCurrentResponse(false); + + ChatItem *thinkingItem = subResponse->subItems.back(); + Q_ASSERT(thinkingItem->type() == ChatItem::Type::Think); + thinkingItem->setCurrentResponse(false); + thinkingItem->setValue(split.first); + thinkingItem->setThinkingTime(thinkingTime); + + currentResponse->setValue(split.second); + } + + emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ChildItemsRole, ContentRole}); + } + + Q_INVOKABLE void splitToolCall(const QPair &split) + { + qsizetype index; + { + QMutexLocker locker(&m_mutex); + if (m_chatItems.isEmpty() || m_chatItems.cend()[-1]->type() != ChatItem::Type::Response) + throw std::logic_error("can only set toolcall on a chat that ends with a response"); + + index = m_chatItems.count() - 1; + ChatItem *currentResponse = m_chatItems.back(); + Q_ASSERT(currentResponse->isCurrentResponse); + + // Create a new response container for any text and the tool call + ChatItem *newResponse = new ChatItem(this, ChatItem::response_tag); + + // Add preceding text if any + if (!split.first.isEmpty()) { + ChatItem *textItem = new ChatItem(this, ChatItem::text_tag, split.first); + newResponse->subItems.push_back(textItem); + } + + // Add the toolcall + Q_ASSERT(!split.second.isEmpty()); + ChatItem *toolCallItem = new ChatItem(this, ChatItem::tool_call_tag, split.second); + toolCallItem->isCurrentResponse = true; + newResponse->subItems.push_back(toolCallItem); + + // Add new response and reset our value + currentResponse->subItems.push_back(newResponse); + currentResponse->value = QString(); + } + + emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ChildItemsRole, ContentRole}); + } + + Q_INVOKABLE void updateToolCall(const ToolCallInfo &toolCallInfo) + { + qsizetype index; + { + QMutexLocker locker(&m_mutex); + if (m_chatItems.isEmpty() || m_chatItems.cend()[-1]->type() != ChatItem::Type::Response) + throw std::logic_error("can only set toolcall on a chat that ends with a response"); + + index = m_chatItems.count() - 1; + ChatItem *currentResponse = m_chatItems.back(); + Q_ASSERT(currentResponse->isCurrentResponse); + + ChatItem *subResponse = currentResponse->subItems.back(); + Q_ASSERT(subResponse->type() == ChatItem::Type::Response); + Q_ASSERT(subResponse->isCurrentResponse); + + ChatItem *toolCallItem = subResponse->subItems.back(); + Q_ASSERT(toolCallItem->type() == ChatItem::Type::ToolCall); + toolCallItem->setToolCallInfo(toolCallInfo); + toolCallItem->setCurrentResponse(false); + + // Add tool response + ChatItem *toolResponseItem = new ChatItem(this, ChatItem::tool_response_tag, toolCallInfo.result); + currentResponse->subItems.push_back(toolResponseItem); + } + + emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ChildItemsRole, ContentRole}); + } + + void clearSubItems(int index) + { + bool changed = false; + { + QMutexLocker locker(&m_mutex); + if (index < 0 || index >= m_chatItems.size()) return; + if (m_chatItems.isEmpty() || m_chatItems[index]->type() != ChatItem::Type::Response) + throw std::logic_error("can only clear subitems on a chat that ends with a response"); + + ChatItem *item = m_chatItems.back(); + if (!item->subItems.empty()) { + item->subItems.clear(); + changed = true; + } + } + if (changed) { + emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ChildItemsRole, ContentRole}); + } + } + + Q_INVOKABLE void setError(bool value = true) + { + qsizetype index; + { + QMutexLocker locker(&m_mutex); + + if (m_chatItems.isEmpty() || m_chatItems.cend()[-1]->type() != ChatItem::Type::Response) + throw std::logic_error("can only set error on a chat that ends with a response"); + + index = m_chatItems.count() - 1; + auto &last = m_chatItems.back(); + if (last->isError == value) + return; // already set + last->isError = value; + } + emit dataChanged(createIndex(index, 0), createIndex(index, 0), {IsErrorRole}); + emit hasErrorChanged(value); + } + + Q_INVOKABLE void copyToClipboard() + { + QMutexLocker locker(&m_mutex); + QString conversation; + for (ChatItem *item : m_chatItems) { + QString string = item->name; + string += item->clipboardContent(); + string += "\n"; + conversation += string; + } + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(conversation, QClipboard::Clipboard); + } + + Q_INVOKABLE void copyToClipboard(int index) + { + QMutexLocker locker(&m_mutex); + if (index < 0 || index >= m_chatItems.size()) + return; + ChatItem *item = m_chatItems.at(index); + QClipboard *clipboard = QGuiApplication::clipboard(); + clipboard->setText(item->clipboardContent(), QClipboard::Clipboard); + } + + qsizetype count() const { QMutexLocker locker(&m_mutex); return m_chatItems.size(); } + + std::vector messageItems() const + { + // A flattened version of the chat item tree used by the backend and jinja + QMutexLocker locker(&m_mutex); + std::vector chatItems; + for (qsizetype i : views::iota(0, m_chatItems.size())) { + auto *parent = m_chatItems.at(i); + chatItems.reserve(chatItems.size() + parent->subItems.size() + 1); + ranges::copy(parent->subItems | views::transform([&](auto *s) { return s->asMessageItem(i); }), + std::back_inserter(chatItems)); + chatItems.push_back(parent->asMessageItem(i)); + } + return chatItems; + } + + bool hasError() const { QMutexLocker locker(&m_mutex); return hasErrorUnlocked(); } bool serialize(QDataStream &stream, int version) const { + // FIXME: need to serialize new chatitem tree QMutexLocker locker(&m_mutex); stream << int(m_chatItems.size()); - for (const auto &c : m_chatItems) { - // FIXME: This 'id' should be eliminated the next time we bump serialization version. - // (Jared) This was apparently never used. - int id = 0; - stream << id; - stream << c.name; - stream << c.value; - stream << c.newResponse; - stream << c.currentResponse; - stream << c.stopped; - stream << c.thumbsUpState; - stream << c.thumbsDownState; - if (version >= 8) { - stream << c.sources.size(); - for (const ResultInfo &info : c.sources) { - Q_ASSERT(!info.file.isEmpty()); - stream << info.collection; - stream << info.path; - stream << info.file; - stream << info.title; - stream << info.author; - stream << info.date; - stream << info.text; - stream << info.page; - stream << info.from; - stream << info.to; - } - } else if (version >= 3) { - QList references; - QList referencesContext; - int validReferenceNumber = 1; - for (const ResultInfo &info : c.sources) { - if (info.file.isEmpty()) - continue; - - QString reference; - { - QTextStream stream(&reference); - stream << (validReferenceNumber++) << ". "; - if (!info.title.isEmpty()) - stream << "\"" << info.title << "\". "; - if (!info.author.isEmpty()) - stream << "By " << info.author << ". "; - if (!info.date.isEmpty()) - stream << "Date: " << info.date << ". "; - stream << "In " << info.file << ". "; - if (info.page != -1) - stream << "Page " << info.page << ". "; - if (info.from != -1) { - stream << "Lines " << info.from; - if (info.to != -1) - stream << "-" << info.to; - stream << ". "; - } - stream << "[Context](context://" << validReferenceNumber - 1 << ")"; + for (auto itemIt = m_chatItems.cbegin(); itemIt < m_chatItems.cend(); ++itemIt) { + auto c = *itemIt; // NB: copies + if (version < 11) { + // move sources from their prompt to the next response + switch (c->type()) { + using enum ChatItem::Type; + case Prompt: + c->sources.clear(); + c->consolidatedSources.clear(); + break; + case Response: + // note: we drop sources for responseless prompts + if (auto peer = getPeerUnlocked(itemIt)) { + c->sources = (**peer)->sources; + c->consolidatedSources = (**peer)->consolidatedSources; } - references.append(reference); - referencesContext.append(info.text); - } - - stream << references.join("\n"); - stream << referencesContext; - } - if (version >= 10) { - stream << c.promptAttachments.size(); - for (const PromptAttachment &a : c.promptAttachments) { - Q_ASSERT(!a.url.isEmpty()); - stream << a.url; - stream << a.content; + default: + ; } } + + c->serialize(stream, version); } return stream.status() == QDataStream::Ok; } bool deserialize(QDataStream &stream, int version) { + clear(); // reset to known state + int size; stream >> size; + int lastPromptIndex = -1; + QList chatItems; for (int i = 0; i < size; ++i) { - ChatItem c; - // FIXME: see comment in serialization about id - int id; - stream >> id; - stream >> c.name; - stream >> c.value; - if (version < 10) { - // This is deprecated and no longer used - QString prompt; - stream >> prompt; + ChatItem *c = new ChatItem(this); + if (!c->deserialize(stream, version)) { + delete c; + return false; } - stream >> c.newResponse; - stream >> c.currentResponse; - stream >> c.stopped; - stream >> c.thumbsUpState; - stream >> c.thumbsDownState; - if (version >= 8) { - qsizetype count; - stream >> count; - QList sources; - for (int i = 0; i < count; ++i) { - ResultInfo info; - stream >> info.collection; - stream >> info.path; - stream >> info.file; - stream >> info.title; - stream >> info.author; - stream >> info.date; - stream >> info.text; - stream >> info.page; - stream >> info.from; - stream >> info.to; - sources.append(info); + if (version < 11 && c->type() == ChatItem::Type::Response) { + // move sources from the response to their last prompt + if (lastPromptIndex >= 0) { + auto &prompt = chatItems[lastPromptIndex]; + prompt->sources = std::move(c->sources ); + prompt->consolidatedSources = std::move(c->consolidatedSources); + lastPromptIndex = -1; + } else { + // drop sources for promptless responses + c->sources.clear(); + c->consolidatedSources.clear(); } - c.sources = sources; - c.consolidatedSources = consolidateSources(sources); - } else if (version >= 3) { - QString references; - QList referencesContext; - stream >> references; - stream >> referencesContext; - - if (!references.isEmpty()) { - QList sources; - QList referenceList = references.split("\n"); - - // Ignore empty lines and those that begin with "---" which is no longer used - for (auto it = referenceList.begin(); it != referenceList.end();) { - if (it->trimmed().isEmpty() || it->trimmed().startsWith("---")) - it = referenceList.erase(it); - else - ++it; - } + } - Q_ASSERT(referenceList.size() == referencesContext.size()); - for (int j = 0; j < referenceList.size(); ++j) { - QString reference = referenceList[j]; - QString context = referencesContext[j]; - ResultInfo info; - QTextStream refStream(&reference); - QString dummy; - int validReferenceNumber; - refStream >> validReferenceNumber >> dummy; - // Extract title (between quotes) - if (reference.contains("\"")) { - int startIndex = reference.indexOf('"') + 1; - int endIndex = reference.indexOf('"', startIndex); - info.title = reference.mid(startIndex, endIndex - startIndex); - } - - // Extract author (after "By " and before the next period) - if (reference.contains("By ")) { - int startIndex = reference.indexOf("By ") + 3; - int endIndex = reference.indexOf('.', startIndex); - info.author = reference.mid(startIndex, endIndex - startIndex).trimmed(); - } - - // Extract date (after "Date: " and before the next period) - if (reference.contains("Date: ")) { - int startIndex = reference.indexOf("Date: ") + 6; - int endIndex = reference.indexOf('.', startIndex); - info.date = reference.mid(startIndex, endIndex - startIndex).trimmed(); - } - - // Extract file name (after "In " and before the "[Context]") - if (reference.contains("In ") && reference.contains(". [Context]")) { - int startIndex = reference.indexOf("In ") + 3; - int endIndex = reference.indexOf(". [Context]", startIndex); - info.file = reference.mid(startIndex, endIndex - startIndex).trimmed(); - } - - // Extract page number (after "Page " and before the next space) - if (reference.contains("Page ")) { - int startIndex = reference.indexOf("Page ") + 5; - int endIndex = reference.indexOf(' ', startIndex); - if (endIndex == -1) endIndex = reference.length(); - info.page = reference.mid(startIndex, endIndex - startIndex).toInt(); - } - - // Extract lines (after "Lines " and before the next space or hyphen) - if (reference.contains("Lines ")) { - int startIndex = reference.indexOf("Lines ") + 6; - int endIndex = reference.indexOf(' ', startIndex); - if (endIndex == -1) endIndex = reference.length(); - int hyphenIndex = reference.indexOf('-', startIndex); - if (hyphenIndex != -1 && hyphenIndex < endIndex) { - info.from = reference.mid(startIndex, hyphenIndex - startIndex).toInt(); - info.to = reference.mid(hyphenIndex + 1, endIndex - hyphenIndex - 1).toInt(); - } else { - info.from = reference.mid(startIndex, endIndex - startIndex).toInt(); - } - } - info.text = context; - sources.append(info); - } + chatItems << c; + if (c->type() == ChatItem::Type::Prompt) + lastPromptIndex = chatItems.size() - 1; + } - c.sources = sources; - c.consolidatedSources = consolidateSources(sources); - } - } - if (version >= 10) { - qsizetype count; - stream >> count; - QList attachments; - for (int i = 0; i < count; ++i) { - PromptAttachment a; - stream >> a.url; - stream >> a.content; - attachments.append(a); - } - c.promptAttachments = attachments; - } - m_mutex.lock(); - const int count = m_chatItems.size(); - m_mutex.unlock(); - beginInsertRows(QModelIndex(), count, count); - { - QMutexLocker locker(&m_mutex); - m_chatItems.append(c); - } - endInsertRows(); + bool hasError; + beginInsertRows(QModelIndex(), 0, chatItems.size() - 1 /*inclusive*/); + { + QMutexLocker locker(&m_mutex); + m_chatItems = chatItems; + hasError = hasErrorUnlocked(); } + endInsertRows(); emit countChanged(); + if (hasError) + emit hasErrorChanged(true); return stream.status() == QDataStream::Ok; } Q_SIGNALS: void countChanged(); - void valueChanged(int index, const QString &value); + void hasErrorChanged(bool value); + +private: + bool hasErrorUnlocked() const + { + if (m_chatItems.isEmpty()) + return false; + auto &last = m_chatItems.back(); + return last->type() == ChatItem::Type::Response && last->isError; + } private: mutable QMutex m_mutex; - QList m_chatItems; + QList m_chatItems; }; #endif // CHATMODEL_H diff --git a/gpt4all-chat/src/codeinterpreter.cpp b/gpt4all-chat/src/codeinterpreter.cpp new file mode 100644 index 000000000000..09d95b379376 --- /dev/null +++ b/gpt4all-chat/src/codeinterpreter.cpp @@ -0,0 +1,175 @@ +#include "codeinterpreter.h" + +#include +#include +#include +#include + +using namespace Qt::Literals::StringLiterals; + +CodeInterpreter::CodeInterpreter() + : Tool() + , m_error(ToolEnums::Error::NoError) +{ + m_worker = new CodeInterpreterWorker; + connect(this, &CodeInterpreter::request, m_worker, &CodeInterpreterWorker::request, Qt::QueuedConnection); +} + +void CodeInterpreter::run(const QList ¶ms) +{ + m_error = ToolEnums::Error::NoError; + m_errorString = QString(); + + Q_ASSERT(params.count() == 1 + && params.first().name == "code" + && params.first().type == ToolEnums::ParamType::String); + + const QString code = params.first().value.toString(); + connect(m_worker, &CodeInterpreterWorker::finished, [this, params] { + m_error = m_worker->error(); + m_errorString = m_worker->errorString(); + emit runComplete({ + ToolCallConstants::CodeInterpreterFunction, + params, + m_worker->response(), + m_error, + m_errorString + }); + }); + + emit request(code); +} + +bool CodeInterpreter::interrupt() +{ + return m_worker->interrupt(); +} + +QList CodeInterpreter::parameters() const +{ + return {{ + "code", + ToolEnums::ParamType::String, + "javascript code to compute", + true + }}; +} + +QString CodeInterpreter::symbolicFormat() const +{ + return "{human readable plan to complete the task}\n" + ToolCallConstants::CodeInterpreterPrefix + "{code}\n" + ToolCallConstants::CodeInterpreterSuffix; +} + +QString CodeInterpreter::examplePrompt() const +{ + return R"(Write code to check if a number is prime, use that to see if the number 7 is prime)"; +} + +QString CodeInterpreter::exampleCall() const +{ + static const QString example = R"(function isPrime(n) { + if (n <= 1) { + return false; + } + for (let i = 2; i <= Math.sqrt(n); i++) { + if (n % i === 0) { + return false; + } + } + return true; +} + +const number = 7; +console.log(`The number ${number} is prime: ${isPrime(number)}`); +)"; + + return "Certainly! Let's compute the answer to whether the number 7 is prime.\n" + ToolCallConstants::CodeInterpreterPrefix + example + ToolCallConstants::CodeInterpreterSuffix; +} + +QString CodeInterpreter::exampleReply() const +{ + return R"("The computed result shows that 7 is a prime number.)"; +} + +CodeInterpreterWorker::CodeInterpreterWorker() + : QObject(nullptr) + , m_engine(new QJSEngine(this)) +{ + moveToThread(&m_thread); + + QJSValue consoleInternalObject = m_engine->newQObject(&m_consoleCapture); + m_engine->globalObject().setProperty("console_internal", consoleInternalObject); + + // preprocess console.log args in JS since Q_INVOKE doesn't support varargs + auto consoleObject = m_engine->evaluate(uR"( + class Console { + log(...args) { + if (args.length == 0) + return; + if (args.length >= 2 && typeof args[0] === 'string') + throw new Error('console.log string formatting not supported'); + let cat = ''; + for (const arg of args) { + cat += String(arg); + } + console_internal.log(cat); + } + } + + new Console(); + )"_s); + m_engine->globalObject().setProperty("console", consoleObject); + m_thread.start(); +} + +void CodeInterpreterWorker::reset() +{ + m_response.clear(); + m_error = ToolEnums::Error::NoError; + m_errorString.clear(); + m_consoleCapture.output.clear(); + m_engine->setInterrupted(false); +} + +void CodeInterpreterWorker::request(const QString &code) +{ + reset(); + const QJSValue result = m_engine->evaluate(code); + QString resultString; + + if (m_engine->isInterrupted()) { + resultString = QString("Error: code execution was interrupted or timed out."); + } else if (result.isError()) { + // NOTE: We purposely do not set the m_error or m_errorString for the code interpreter since + // we *want* the model to see the response has an error so it can hopefully correct itself. The + // error member variables are intended for tools that have error conditions that cannot be corrected. + // For instance, a tool depending upon the network might set these error variables if the network + // is not available. + const QStringList lines = code.split('\n'); + const int line = result.property("lineNumber").toInt(); + const int index = line - 1; + const QString lineContent = (index >= 0 && index < lines.size()) ? lines.at(index) : "Line not found in code."; + resultString = QString("Uncaught exception at line %1: %2\n\t%3") + .arg(line) + .arg(result.toString()) + .arg(lineContent); + m_error = ToolEnums::Error::UnknownError; + m_errorString = resultString; + } else { + resultString = result.isUndefined() ? QString() : result.toString(); + } + + if (resultString.isEmpty()) + resultString = m_consoleCapture.output; + else if (!m_consoleCapture.output.isEmpty()) + resultString += "\n" + m_consoleCapture.output; + m_response = resultString; + emit finished(); +} + +bool CodeInterpreterWorker::interrupt() +{ + m_error = ToolEnums::Error::TimeoutError; + m_engine->setInterrupted(true); + return true; +} diff --git a/gpt4all-chat/src/codeinterpreter.h b/gpt4all-chat/src/codeinterpreter.h new file mode 100644 index 000000000000..f28439d0f9dc --- /dev/null +++ b/gpt4all-chat/src/codeinterpreter.h @@ -0,0 +1,95 @@ +#ifndef CODEINTERPRETER_H +#define CODEINTERPRETER_H + +#include "tool.h" +#include "toolcallparser.h" + +#include +#include +#include +#include +#include + +class JavaScriptConsoleCapture : public QObject +{ + Q_OBJECT +public: + QString output; + Q_INVOKABLE void log(const QString &message) + { + const int maxLength = 1024; + if (output.length() >= maxLength) + return; + + if (output.length() + message.length() + 1 > maxLength) { + static const QString trunc = "\noutput truncated at " + QString::number(maxLength) + " characters..."; + int remainingLength = maxLength - output.length(); + if (remainingLength > 0) + output.append(message.left(remainingLength)); + output.append(trunc); + Q_ASSERT(output.length() > maxLength); + } else { + output.append(message + "\n"); + } + } +}; + +class CodeInterpreterWorker : public QObject { + Q_OBJECT +public: + CodeInterpreterWorker(); + virtual ~CodeInterpreterWorker() {} + + void reset(); + QString response() const { return m_response; } + ToolEnums::Error error() const { return m_error; } + QString errorString() const { return m_errorString; } + bool interrupt(); + +public Q_SLOTS: + void request(const QString &code); + +Q_SIGNALS: + void finished(); + +private: + QString m_response; + ToolEnums::Error m_error = ToolEnums::Error::NoError; + QString m_errorString; + QThread m_thread; + JavaScriptConsoleCapture m_consoleCapture; + QJSEngine *m_engine = nullptr; +}; + +class CodeInterpreter : public Tool +{ + Q_OBJECT +public: + explicit CodeInterpreter(); + virtual ~CodeInterpreter() {} + + void run(const QList ¶ms) override; + bool interrupt() override; + + ToolEnums::Error error() const override { return m_error; } + QString errorString() const override { return m_errorString; } + + QString name() const override { return tr("Code Interpreter"); } + QString description() const override { return tr("compute javascript code using console.log as output"); } + QString function() const override { return ToolCallConstants::CodeInterpreterFunction; } + QList parameters() const override; + virtual QString symbolicFormat() const override; + QString examplePrompt() const override; + QString exampleCall() const override; + QString exampleReply() const override; + +Q_SIGNALS: + void request(const QString &code); + +private: + ToolEnums::Error m_error = ToolEnums::Error::NoError; + QString m_errorString; + CodeInterpreterWorker *m_worker; +}; + +#endif // CODEINTERPRETER_H diff --git a/gpt4all-chat/src/config.h.in b/gpt4all-chat/src/config.h.in new file mode 100644 index 000000000000..c919b4bafa96 --- /dev/null +++ b/gpt4all-chat/src/config.h.in @@ -0,0 +1,7 @@ +#pragma once + +#define APP_VERSION "@APP_VERSION@" + +#define G4A_CONFIG(name) (1/G4A_CONFIG_##name == 1) + +#define G4A_CONFIG_force_d3d12 @GPT4ALL_CONFIG_FORCE_D3D12@ diff --git a/gpt4all-chat/src/database.cpp b/gpt4all-chat/src/database.cpp index 5ea2fd173639..b7b509c676e2 100644 --- a/gpt4all-chat/src/database.cpp +++ b/gpt4all-chat/src/database.cpp @@ -7,14 +7,13 @@ #include #include +#include #include #include #include #include #include #include -#include -#include #include #include #include @@ -31,6 +30,15 @@ #include #include +#ifdef GPT4ALL_USE_QTPDF +# include +# include +#else +# include +# include +# include +#endif + using namespace Qt::Literals::StringLiterals; namespace ranges = std::ranges; namespace us = unum::usearch; @@ -1133,6 +1141,7 @@ class DocumentReader { namespace { +#ifdef GPT4ALL_USE_QTPDF class PdfDocumentReader final : public DocumentReader { public: explicit PdfDocumentReader(const DocumentInfo &info) @@ -1173,6 +1182,99 @@ class PdfDocumentReader final : public DocumentReader { QString m_pageText; std::optional m_stream; }; +#else +class PdfDocumentReader final : public DocumentReader { +public: + explicit PdfDocumentReader(const DocumentInfo &info) + : DocumentReader(info) + { + QString path = info.file.canonicalFilePath(); + m_doc = FPDF_LoadDocument(path.toUtf8().constData(), nullptr); + if (!m_doc) + throw std::runtime_error(fmt::format("Failed to load PDF: {}", path)); + + // Extract metadata + Metadata metadata { + .title = getMetadata("Title" ), + .author = getMetadata("Author" ), + .subject = getMetadata("Subject" ), + .keywords = getMetadata("Keywords"), + }; + postInit(std::move(metadata)); + } + + ~PdfDocumentReader() override + { + if (m_page) + FPDF_ClosePage(m_page); + if (m_doc) + FPDF_CloseDocument(m_doc); + FPDF_DestroyLibrary(); + } + + int page() const override { return m_currentPage; } + +private: + std::optional advance() override + { + QString word; + do { + while (!m_stream || m_stream->atEnd()) { + if (m_currentPage >= FPDF_GetPageCount(m_doc)) + return std::nullopt; + + if (m_page) + FPDF_ClosePage(m_page); + m_page = FPDF_LoadPage(m_doc, m_currentPage++); + if (!m_page) + throw std::runtime_error("Failed to load page."); + + m_pageText = extractTextFromPage(m_page); + m_stream.emplace(&m_pageText); + } + *m_stream >> word; + } while (word.isEmpty()); + return word; + } + + QString getMetadata(FPDF_BYTESTRING key) + { + // FPDF_GetMetaText includes a 2-byte null terminator + ulong nBytes = FPDF_GetMetaText(m_doc, key, nullptr, 0); + if (nBytes <= sizeof (FPDF_WCHAR)) + return { "" }; + QByteArray buffer(nBytes, Qt::Uninitialized); + ulong nResultBytes = FPDF_GetMetaText(m_doc, key, buffer.data(), buffer.size()); + Q_ASSERT(nResultBytes % 2 == 0); + Q_ASSERT(nResultBytes <= nBytes); + return QString::fromUtf16(reinterpret_cast(buffer.data()), nResultBytes / 2 - 1); + } + + QString extractTextFromPage(FPDF_PAGE page) + { + FPDF_TEXTPAGE textPage = FPDFText_LoadPage(page); + if (!textPage) + throw std::runtime_error("Failed to load text page."); + + int nChars = FPDFText_CountChars(textPage); + if (!nChars) + return {}; + // FPDFText_GetText includes a 2-byte null terminator + QByteArray buffer((nChars + 1) * sizeof (FPDF_WCHAR), Qt::Uninitialized); + int nResultChars = FPDFText_GetText(textPage, 0, nChars, reinterpret_cast(buffer.data())); + Q_ASSERT(nResultChars <= nChars + 1); + + FPDFText_ClosePage(textPage); + return QString::fromUtf16(reinterpret_cast(buffer.data()), nResultChars - 1); + } + + FPDF_DOCUMENT m_doc = nullptr; + FPDF_PAGE m_page = nullptr; + int m_currentPage = 0; + QString m_pageText; + std::optional m_stream; +}; +#endif // !defined(GPT4ALL_USE_QTPDF) class WordDocumentReader final : public DocumentReader { public: diff --git a/gpt4all-chat/src/jinja_helpers.cpp b/gpt4all-chat/src/jinja_helpers.cpp new file mode 100644 index 000000000000..9ae11f7d490b --- /dev/null +++ b/gpt4all-chat/src/jinja_helpers.cpp @@ -0,0 +1,81 @@ +#include "jinja_helpers.h" + +#include "utils.h" + +#include + +#include +#include + +#include +#include +#include +#include + +namespace views = std::views; +using json = nlohmann::ordered_json; + + +json::object_t JinjaResultInfo::AsJson() const +{ + return { + { "collection", m_source->collection.toStdString() }, + { "path", m_source->path .toStdString() }, + { "file", m_source->file .toStdString() }, + { "title", m_source->title .toStdString() }, + { "author", m_source->author .toStdString() }, + { "date", m_source->date .toStdString() }, + { "text", m_source->text .toStdString() }, + { "page", m_source->page }, + { "file_uri", m_source->fileUri() .toStdString() }, + }; +} + +json::object_t JinjaPromptAttachment::AsJson() const +{ + return { + { "url", m_attachment->url.toString() .toStdString() }, + { "file", m_attachment->file() .toStdString() }, + { "processed_content", m_attachment->processedContent().toStdString() }, + }; +} + +json::object_t JinjaMessage::AsJson() const +{ + json::object_t obj; + { + json::string_t role; + switch (m_item->type()) { + using enum MessageItem::Type; + case System: role = "system"; break; + case Prompt: role = "user"; break; + case Response: role = "assistant"; break; + case ToolResponse: role = "tool"; break; + } + obj.emplace_back("role", std::move(role)); + } + { + QString content; + if (m_version == 0 && m_item->type() == MessageItem::Type::Prompt) { + content = m_item->bakedPrompt(); + } else { + content = m_item->content(); + } + obj.emplace_back("content", content.toStdString()); + } + if (m_item->type() == MessageItem::Type::Prompt) { + { + auto sources = m_item->sources() | views::transform([](auto &r) { + return JinjaResultInfo(r).AsJson(); + }); + obj.emplace("sources", json::array_t(sources.begin(), sources.end())); + } + { + auto attachments = m_item->promptAttachments() | views::transform([](auto &pa) { + return JinjaPromptAttachment(pa).AsJson(); + }); + obj.emplace("prompt_attachments", json::array_t(attachments.begin(), attachments.end())); + } + } + return obj; +} diff --git a/gpt4all-chat/src/jinja_helpers.h b/gpt4all-chat/src/jinja_helpers.h new file mode 100644 index 000000000000..75dc1776f9a7 --- /dev/null +++ b/gpt4all-chat/src/jinja_helpers.h @@ -0,0 +1,51 @@ +#pragma once + +#include "chatmodel.h" +#include "database.h" + +#include + +#include + +using json = nlohmann::ordered_json; + + +template +class JinjaHelper { +public: + json::object_t AsJson() const { return static_cast(this)->AsJson(); } +}; + +class JinjaResultInfo : public JinjaHelper { +public: + explicit JinjaResultInfo(const ResultInfo &source) noexcept + : m_source(&source) {} + + json::object_t AsJson() const; + +private: + const ResultInfo *m_source; +}; + +class JinjaPromptAttachment : public JinjaHelper { +public: + explicit JinjaPromptAttachment(const PromptAttachment &attachment) noexcept + : m_attachment(&attachment) {} + + json::object_t AsJson() const; + +private: + const PromptAttachment *m_attachment; +}; + +class JinjaMessage : public JinjaHelper { +public: + explicit JinjaMessage(uint version, const MessageItem &item) noexcept + : m_version(version), m_item(&item) {} + + json::object_t AsJson() const; + +private: + uint m_version; + const MessageItem *m_item; +}; diff --git a/gpt4all-chat/src/jinja_replacements.cpp b/gpt4all-chat/src/jinja_replacements.cpp new file mode 100644 index 000000000000..d5e180746af5 --- /dev/null +++ b/gpt4all-chat/src/jinja_replacements.cpp @@ -0,0 +1,687 @@ +// The map in this file is automatically generated by Jared. Do not hand-edit it. + +#include "jinja_replacements.h" + +// This is a list of prompt templates known to GPT4All and their associated replacements which are automatically used +// instead when loading the chat template from GGUF. These exist for two primary reasons: +// - HuggingFace model authors make ugly chat templates because they do not expect the end user to see them; +// - and chat templates occasionally use features we do not support. This is less true now that we use minja. + +// The substitution list. + +const std::unordered_map CHAT_TEMPLATE_SUBSTITUTIONS { + // calme-2.1-phi3.5-4b.Q6_K.gguf (reported by ThilotE on Discord), Phi-3.5-mini-instruct-Q4_0.gguf (nomic-ai/gpt4all#3345) + { + // original + R"TEMPLATE({% for message in messages %}{% if message['role'] == 'system' and message['content'] %}{{'<|system|> +' + message['content'] + '<|end|> +'}}{% elif message['role'] == 'user' %}{{'<|user|> +' + message['content'] + '<|end|> +'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|> +' + message['content'] + '<|end|> +'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|> +' }}{% else %}{{ eos_token }}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({%- for message in messages %} + {%- if message['role'] == 'system' and message['content'] %} + {{- '<|system|>\n' + message['content'] + '<|end|>\n' }} + {%- elif message['role'] == 'user' %} + {{- '<|user|>\n' + message['content'] + '<|end|>\n' }} + {%- elif message['role'] == 'assistant' %} + {{- '<|assistant|>\n' + message['content'] + '<|end|>\n' }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|assistant|>\n' }} +{%- else %} + {{- eos_token }} +{%- endif %})TEMPLATE", + }, + // DeepSeek-R1-Distill-Qwen-7B-Q4_0.gguf + { + // original + R"TEMPLATE({% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='') %}{%- for message in messages %}{%- if message['role'] == 'system' %}{% set ns.system_prompt = message['content'] %}{%- endif %}{%- endfor %}{{bos_token}}{{ns.system_prompt}}{%- for message in messages %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{{'<|User|>' + message['content']}}{%- endif %}{%- if message['role'] == 'assistant' and message['content'] is none %}{%- set ns.is_tool = false -%}{%- for tool in message['tool_calls']%}{%- if not ns.is_first %}{{'<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\n' + '```json' + '\n' + tool['function']['arguments'] + '\n' + '```' + '<|tool▁call▁end|>'}}{%- set ns.is_first = true -%}{%- else %}{{'\n' + '<|tool▁call▁begin|>' + tool['type'] + '<|tool▁sep|>' + tool['function']['name'] + '\n' + '```json' + '\n' + tool['function']['arguments'] + '\n' + '```' + '<|tool▁call▁end|>'}}{{'<|tool▁calls▁end|><|end▁of▁sentence|>'}}{%- endif %}{%- endfor %}{%- endif %}{%- if message['role'] == 'assistant' and message['content'] is not none %}{%- if ns.is_tool %}{{'<|tool▁outputs▁end|>' + message['content'] + '<|end▁of▁sentence|>'}}{%- set ns.is_tool = false -%}{%- else %}{% set content = message['content'] %}{% if '' in content %}{% set content = content.split('')[-1] %}{% endif %}{{'<|Assistant|>' + content + '<|end▁of▁sentence|>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_tool = true -%}{%- if ns.is_output_first %}{{'<|tool▁outputs▁begin|><|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- set ns.is_output_first = false %}{%- else %}{{'\n<|tool▁output▁begin|>' + message['content'] + '<|tool▁output▁end|>'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{'<|tool▁outputs▁end|>'}}{% endif %}{% if add_generation_prompt and not ns.is_tool %}{{'<|Assistant|>'}}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({%- if not add_generation_prompt is defined %} + {%- set add_generation_prompt = false %} +{%- endif %} +{%- if messages[0]['role'] == 'system' %} + {{- messages[0]['content'] }} +{%- endif %} +{%- for message in messages %} + {%- if message['role'] == 'user' %} + {{- '<|User|>' + message['content'] }} + {%- endif %} + {%- if message['role'] == 'assistant' %} + {%- set content = message['content'] | regex_replace('^[\\s\\S]*', '') %} + {{- '<|Assistant|>' + content + '<|end▁of▁sentence|>' }} + {%- endif %} +{%- endfor -%} +{%- if add_generation_prompt %} + {{- '<|Assistant|>' }} +{%- endif %})TEMPLATE", + }, + // gemma-2-9b-it-Q4_0.gguf (nomic-ai/gpt4all#3282) + { + // original + R"TEMPLATE({{ bos_token }}{% if messages[0]['role'] == 'system' %}{{ raise_exception('System role not supported') }}{% endif %}{% for message in messages %}{% if (message['role'] == 'user') != (loop.index0 % 2 == 0) %}{{ raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }}{% endif %}{% if (message['role'] == 'assistant') %}{% set role = 'model' %}{% else %}{% set role = message['role'] %}{% endif %}{{ '' + role + ' +' + message['content'] | trim + ' +' }}{% endfor %}{% if add_generation_prompt %}{{'model +'}}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({{- bos_token }} +{%- if messages[0]['role'] == 'system' %} + {{- raise_exception('System role not supported') }} +{%- endif %} +{%- for message in messages %} + {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) %} + {{- raise_exception('Conversation roles must alternate user/assistant/user/assistant/...') }} + {%- endif %} + {%- if message['role'] == 'assistant' %} + {%- set role = 'model' %} + {%- else %} + {%- set role = message['role'] %} + {%- endif %} + {{- '' + role + '\n' + message['content'] | trim + '\n' }} +{%- endfor %} +{%- if add_generation_prompt %} + {{- 'model\n' }} +{%- endif %})TEMPLATE", + }, + // ghost-7b-v0.9.1-Q4_0.gguf + { + // original + R"TEMPLATE({% for message in messages %} +{% if message['role'] == 'user' %} +{{ '<|user|> +' + message['content'] + eos_token }} +{% elif message['role'] == 'system' %} +{{ '<|system|> +' + message['content'] + eos_token }} +{% elif message['role'] == 'assistant' %} +{{ '<|assistant|> +' + message['content'] + eos_token }} +{% endif %} +{% if loop.last and add_generation_prompt %} +{{ '<|assistant|>' }} +{% endif %} +{% endfor %})TEMPLATE", + // replacement + R"TEMPLATE({%- for message in messages %} + {%- if message['role'] == 'user' %} + {{- '<|user|>\n' + message['content'] + eos_token }} + {%- elif message['role'] == 'system' %} + {{- '<|system|>\n' + message['content'] + eos_token }} + {%- elif message['role'] == 'assistant' %} + {{- '<|assistant|>\n' + message['content'] + eos_token }} + {%- endif %} + {%- if loop.last and add_generation_prompt %} + {{- '<|assistant|>' }} + {%- endif %} +{%- endfor %})TEMPLATE", + }, + // Hermes-3-Llama-3.2-3B.Q4_0.gguf, mistral-7b-openorca.gguf2.Q4_0.gguf + { + // original + R"TEMPLATE({% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in messages %}{{'<|im_start|>' + message['role'] + ' +' + message['content'] + '<|im_end|>' + ' +'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant +' }}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({%- for message in messages %} + {{- '<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>\n' }} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|im_start|>assistant\n' }} +{%- endif %})TEMPLATE", + }, + // Llama-3.2-1B-Instruct-Q4_0.gguf, Llama-3.2-3B-Instruct-Q4_0.gguf, SummLlama3.2-3B-Q4_0.gguf (nomic-ai/gpt4all#3309) + { + // original + R"TEMPLATE({{- bos_token }} +{%- if custom_tools is defined %} + {%- set tools = custom_tools %} +{%- endif %} +{%- if not tools_in_user_message is defined %} + {%- set tools_in_user_message = true %} +{%- endif %} +{%- if not date_string is defined %} + {%- if strftime_now is defined %} + {%- set date_string = strftime_now("%d %b %Y") %} + {%- else %} + {%- set date_string = "26 Jul 2024" %} + {%- endif %} +{%- endif %} +{%- if not tools is defined %} + {%- set tools = none %} +{%- endif %} + +{#- This block extracts the system message, so we can slot it into the right place. #} +{%- if messages[0]['role'] == 'system' %} + {%- set system_message = messages[0]['content']|trim %} + {%- set messages = messages[1:] %} +{%- else %} + {%- set system_message = "" %} +{%- endif %} + +{#- System message #} +{{- "<|start_header_id|>system<|end_header_id|>\n\n" }} +{%- if tools is not none %} + {{- "Environment: ipython\n" }} +{%- endif %} +{{- "Cutting Knowledge Date: December 2023\n" }} +{{- "Today Date: " + date_string + "\n\n" }} +{%- if tools is not none and not tools_in_user_message %} + {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }} + {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }} + {{- "Do not use variables.\n\n" }} + {%- for t in tools %} + {{- t | tojson(indent=4) }} + {{- "\n\n" }} + {%- endfor %} +{%- endif %} +{{- system_message }} +{{- "<|eot_id|>" }} + +{#- Custom tools are passed in a user message with some extra guidance #} +{%- if tools_in_user_message and not tools is none %} + {#- Extract the first user message so we can plug it in here #} + {%- if messages | length != 0 %} + {%- set first_user_message = messages[0]['content']|trim %} + {%- set messages = messages[1:] %} + {%- else %} + {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }} +{%- endif %} + {{- '<|start_header_id|>user<|end_header_id|>\n\n' -}} + {{- "Given the following functions, please respond with a JSON for a function call " }} + {{- "with its proper arguments that best answers the given prompt.\n\n" }} + {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }} + {{- "Do not use variables.\n\n" }} + {%- for t in tools %} + {{- t | tojson(indent=4) }} + {{- "\n\n" }} + {%- endfor %} + {{- first_user_message + "<|eot_id|>"}} +{%- endif %} + +{%- for message in messages %} + {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' }} + {%- elif 'tool_calls' in message %} + {%- if not message.tool_calls|length == 1 %} + {{- raise_exception("This model only supports single tool-calls at once!") }} + {%- endif %} + {%- set tool_call = message.tool_calls[0].function %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}} + {{- '{"name": "' + tool_call.name + '", ' }} + {{- '"parameters": ' }} + {{- tool_call.arguments | tojson }} + {{- "}" }} + {{- "<|eot_id|>" }} + {%- elif message.role == "tool" or message.role == "ipython" %} + {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }} + {%- if message.content is mapping or message.content is iterable %} + {{- message.content | tojson }} + {%- else %} + {{- message.content }} + {%- endif %} + {{- "<|eot_id|>" }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %})TEMPLATE", + // replacement + R"TEMPLATE({{- bos_token }} +{%- set date_string = strftime_now('%d %b %Y') %} + +{#- This block extracts the system message, so we can slot it into the right place. #} +{%- if messages[0]['role'] == 'system' %} + {%- set system_message = messages[0]['content'] | trim %} + {%- set loop_start = 1 %} +{%- else %} + {%- set system_message = '' %} + {%- set loop_start = 0 %} +{%- endif %} + +{#- System message #} +{{- '<|start_header_id|>system<|end_header_id|>\n\n' }} +{{- 'Cutting Knowledge Date: December 2023\n' }} +{{- 'Today Date: ' + date_string + '\n\n' }} +{{- system_message }} +{{- '<|eot_id|>' }} + +{%- for message in messages %} + {%- if loop.index0 >= loop_start %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + message['content'] | trim + '<|eot_id|>' }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %})TEMPLATE", + }, + // Llama-3.3-70B-Instruct-Q4_0.gguf (nomic-ai/gpt4all#3305) + { + // original + R"TEMPLATE({{- bos_token }} +{%- if custom_tools is defined %} + {%- set tools = custom_tools %} +{%- endif %} +{%- if not tools_in_user_message is defined %} + {%- set tools_in_user_message = true %} +{%- endif %} +{%- if not date_string is defined %} + {%- set date_string = "26 Jul 2024" %} +{%- endif %} +{%- if not tools is defined %} + {%- set tools = none %} +{%- endif %} + +{#- This block extracts the system message, so we can slot it into the right place. #} +{%- if messages[0]['role'] == 'system' %} + {%- set system_message = messages[0]['content']|trim %} + {%- set messages = messages[1:] %} +{%- else %} + {%- set system_message = "" %} +{%- endif %} + +{#- System message + builtin tools #} +{{- "<|start_header_id|>system<|end_header_id|>\n\n" }} +{%- if builtin_tools is defined or tools is not none %} + {{- "Environment: ipython\n" }} +{%- endif %} +{%- if builtin_tools is defined %} + {{- "Tools: " + builtin_tools | reject('equalto', 'code_interpreter') | join(", ") + "\n\n"}} +{%- endif %} +{{- "Cutting Knowledge Date: December 2023\n" }} +{{- "Today Date: " + date_string + "\n\n" }} +{%- if tools is not none and not tools_in_user_message %} + {{- "You have access to the following functions. To call a function, please respond with JSON for a function call." }} + {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }} + {{- "Do not use variables.\n\n" }} + {%- for t in tools %} + {{- t | tojson(indent=4) }} + {{- "\n\n" }} + {%- endfor %} +{%- endif %} +{{- system_message }} +{{- "<|eot_id|>" }} + +{#- Custom tools are passed in a user message with some extra guidance #} +{%- if tools_in_user_message and not tools is none %} + {#- Extract the first user message so we can plug it in here #} + {%- if messages | length != 0 %} + {%- set first_user_message = messages[0]['content']|trim %} + {%- set messages = messages[1:] %} + {%- else %} + {{- raise_exception("Cannot put tools in the first user message when there's no first user message!") }} +{%- endif %} + {{- '<|start_header_id|>user<|end_header_id|>\n\n' -}} + {{- "Given the following functions, please respond with a JSON for a function call " }} + {{- "with its proper arguments that best answers the given prompt.\n\n" }} + {{- 'Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.' }} + {{- "Do not use variables.\n\n" }} + {%- for t in tools %} + {{- t | tojson(indent=4) }} + {{- "\n\n" }} + {%- endfor %} + {{- first_user_message + "<|eot_id|>"}} +{%- endif %} + +{%- for message in messages %} + {%- if not (message.role == 'ipython' or message.role == 'tool' or 'tool_calls' in message) %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' }} + {%- elif 'tool_calls' in message %} + {%- if not message.tool_calls|length == 1 %} + {{- raise_exception("This model only supports single tool-calls at once!") }} + {%- endif %} + {%- set tool_call = message.tool_calls[0].function %} + {%- if builtin_tools is defined and tool_call.name in builtin_tools %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}} + {{- "<|python_tag|>" + tool_call.name + ".call(" }} + {%- for arg_name, arg_val in tool_call.arguments | items %} + {{- arg_name + '="' + arg_val + '"' }} + {%- if not loop.last %} + {{- ", " }} + {%- endif %} + {%- endfor %} + {{- ")" }} + {%- else %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' -}} + {{- '{"name": "' + tool_call.name + '", ' }} + {{- '"parameters": ' }} + {{- tool_call.arguments | tojson }} + {{- "}" }} + {%- endif %} + {%- if builtin_tools is defined %} + {#- This means we're in ipython mode #} + {{- "<|eom_id|>" }} + {%- else %} + {{- "<|eot_id|>" }} + {%- endif %} + {%- elif message.role == "tool" or message.role == "ipython" %} + {{- "<|start_header_id|>ipython<|end_header_id|>\n\n" }} + {%- if message.content is mapping or message.content is iterable %} + {{- message.content | tojson }} + {%- else %} + {{- message.content }} + {%- endif %} + {{- "<|eot_id|>" }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %})TEMPLATE", + // replacement + R"TEMPLATE({{- bos_token }} +{%- set date_string = strftime_now('%d %b %Y') %} + +{#- This block extracts the system message, so we can slot it into the right place. #} +{%- if messages[0]['role'] == 'system' %} + {%- set system_message = messages[0]['content'] | trim %} + {%- set loop_start = 1 %} +{%- else %} + {%- set system_message = '' %} + {%- set loop_start = 0 %} +{%- endif %} + +{#- System message #} +{{- '<|start_header_id|>system<|end_header_id|>\n\n' }} +{{- 'Cutting Knowledge Date: December 2023\n' }} +{{- 'Today Date: ' + date_string + '\n\n' }} +{{- system_message }} +{{- '<|eot_id|>' }} + +{%- for message in messages %} + {%- if loop.index0 >= loop_start %} + {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + message['content'] | trim + '<|eot_id|>' }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %})TEMPLATE", + }, + // Llama3-DiscoLeo-Instruct-8B-32k-v0.1-Q4_0.gguf (nomic-ai/gpt4all#3347) + { + // original + R"TEMPLATE({% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|> + +'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|> + +' }}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({%- for message in messages %} + {%- set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + message['content'] | trim + '<|eot_id|>' %} + {%- if loop.index0 == 0 %} + {%- set content = bos_token + content %} + {%- endif %} + {{- content }} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %})TEMPLATE", + }, + // Meta-Llama-3.1-8B-Instruct-128k-Q4_0.gguf + { + // original + R"TEMPLATE({% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|> + +'+ message['content'] | trim + '<|eot_id|>' %}{% if loop.index0 == 0 %}{% set content = bos_token + content %}{% endif %}{{ content }}{% endfor %}{{ '<|start_header_id|>assistant<|end_header_id|> + +' }})TEMPLATE", + // replacement + R"TEMPLATE({%- set loop_messages = messages %} +{%- for message in loop_messages %} + {%- set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %} + {%- if loop.index0 == 0 %} + {%- set content = bos_token + content %} + {%- endif %} + {{- content }} +{%- endfor %} +{{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }})TEMPLATE", + }, + // Meta-Llama-3-8B-Instruct.Q4_0.gguf + { + // original + R"TEMPLATE({% set loop_messages = messages %}{% for message in loop_messages %}{% set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|> + +'+ message['content'] | trim + '<|eot_id|>' %}{{ content }}{% endfor %}{% if add_generation_prompt %}{{ '<|start_header_id|>assistant<|end_header_id|> + +' }}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({%- set loop_messages = messages %} +{%- for message in loop_messages %} + {%- set content = '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n'+ message['content'] | trim + '<|eot_id|>' %} + {{- content }} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} +{%- endif %})TEMPLATE", + }, + // Mistral-Nemo-Instruct-2407-Q4_0.gguf (nomic-ai/gpt4all#3284) + { + // original + R"TEMPLATE({%- if messages[0]["role"] == "system" %} + {%- set system_message = messages[0]["content"] %} + {%- set loop_messages = messages[1:] %} +{%- else %} + {%- set loop_messages = messages %} +{%- endif %} +{%- if not tools is defined %} + {%- set tools = none %} +{%- endif %} +{%- set user_messages = loop_messages | selectattr("role", "equalto", "user") | list %} + +{#- This block checks for alternating user/assistant messages, skipping tool calling messages #} +{%- set ns = namespace() %} +{%- set ns.index = 0 %} +{%- for message in loop_messages %} + {%- if not (message.role == "tool" or message.role == "tool_results" or (message.tool_calls is defined and message.tool_calls is not none)) %} + {%- if (message["role"] == "user") != (ns.index % 2 == 0) %} + {{- raise_exception("After the optional system message, conversation roles must alternate user/assistant/user/assistant/...") }} + {%- endif %} + {%- set ns.index = ns.index + 1 %} + {%- endif %} +{%- endfor %} + +{{- bos_token }} +{%- for message in loop_messages %} + {%- if message["role"] == "user" %} + {%- if tools is not none and (message == user_messages[-1]) %} + {{- "[AVAILABLE_TOOLS][" }} + {%- for tool in tools %} + {%- set tool = tool.function %} + {{- '{"type": "function", "function": {' }} + {%- for key, val in tool.items() if key != "return" %} + {%- if val is string %} + {{- '"' + key + '": "' + val + '"' }} + {%- else %} + {{- '"' + key + '": ' + val|tojson }} + {%- endif %} + {%- if not loop.last %} + {{- ", " }} + {%- endif %} + {%- endfor %} + {{- "}}" }} + {%- if not loop.last %} + {{- ", " }} + {%- else %} + {{- "]" }} + {%- endif %} + {%- endfor %} + {{- "[/AVAILABLE_TOOLS]" }} + {%- endif %} + {%- if loop.last and system_message is defined %} + {{- "[INST]" + system_message + "\n\n" + message["content"] + "[/INST]" }} + {%- else %} + {{- "[INST]" + message["content"] + "[/INST]" }} + {%- endif %} + {%- elif (message.tool_calls is defined and message.tool_calls is not none) %} + {{- "[TOOL_CALLS][" }} + {%- for tool_call in message.tool_calls %} + {%- set out = tool_call.function|tojson %} + {{- out[:-1] }} + {%- if not tool_call.id is defined or tool_call.id|length != 9 %} + {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }} + {%- endif %} + {{- ', "id": "' + tool_call.id + '"}' }} + {%- if not loop.last %} + {{- ", " }} + {%- else %} + {{- "]" + eos_token }} + {%- endif %} + {%- endfor %} + {%- elif message["role"] == "assistant" %} + {{- message["content"] + eos_token}} + {%- elif message["role"] == "tool_results" or message["role"] == "tool" %} + {%- if message.content is defined and message.content.content is defined %} + {%- set content = message.content.content %} + {%- else %} + {%- set content = message.content %} + {%- endif %} + {{- '[TOOL_RESULTS]{"content": ' + content|string + ", " }} + {%- if not message.tool_call_id is defined or message.tool_call_id|length != 9 %} + {{- raise_exception("Tool call IDs should be alphanumeric strings with length 9!") }} + {%- endif %} + {{- '"call_id": "' + message.tool_call_id + '"}[/TOOL_RESULTS]' }} + {%- else %} + {{- raise_exception("Only user and assistant roles are supported, with the exception of an initial optional system message!") }} + {%- endif %} +{%- endfor %})TEMPLATE", + // replacement + R"TEMPLATE({%- if messages[0]['role'] == 'system' %} + {%- set system_message = messages[0]['content'] %} + {%- set loop_start = 1 %} +{%- else %} + {%- set loop_start = 0 %} +{%- endif %} + +{{- bos_token }} +{%- for message in messages %} + {#- This block checks for alternating user/assistant messages, skipping tool calling messages #} + {%- if (message['role'] == 'user') != (loop.index0 % 2 == 0) %} + {{- raise_exception('After the optional system message, conversation roles must alternate user/assistant/user/assistant/...') }} + {%- endif %} + + {%- if message['role'] == 'user' %} + {%- if loop.last and loop_start == 1 %} + {{- '[INST]' + system_message + '\n\n' + message['content'] + '[/INST]' }} + {%- else %} + {{- '[INST]' + message['content'] + '[/INST]' }} + {%- endif %} + {%- elif message['role'] == 'assistant' %} + {{- message['content'] + eos_token }} + {%- else %} + {{- raise_exception('Only user and assistant roles are supported, with the exception of an initial optional system message!') }} + {%- endif %} +{%- endfor %})TEMPLATE", + }, + // Nous-Hermes-2-Mistral-7B-DPO.Q4_0.gguf + { + // original + R"TEMPLATE({% for message in messages %}{{'<|im_start|>' + message['role'] + ' +' + message['content'] + '<|im_end|>' + ' +'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant +' }}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({%- for message in messages %} + {{- '<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>\n' }} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|im_start|>assistant\n' }} +{%- endif %})TEMPLATE", + }, + // occiglot-7b-de-en-instruct.Q4_0.gguf (nomic-ai/gpt4all#3283) + { + // original + R"TEMPLATE({{''}}{% if messages[0]['role'] == 'system' %}{% set loop_messages = messages[1:] %}{% set system_message = messages[0]['content'] %}{% else %}{% set loop_messages = messages %}{% set system_message = 'You are a helpful assistant. Please give a long and detailed answer.' %}{% endif %}{% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% for message in loop_messages %}{% if loop.index0 == 0 %}{{'<|im_start|>system +' + system_message + '<|im_end|> +'}}{% endif %}{{'<|im_start|>' + message['role'] + ' +' + message['content'] + '<|im_end|>' + ' +'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant +' }}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({{- bos_token }} +{%- if messages[0]['role'] == 'system' %} + {%- set loop_start = 1 %} + {%- set system_message = messages[0]['content'] %} +{%- else %} + {%- set loop_start = 0 %} + {%- set system_message = 'You are a helpful assistant. Please give a long and detailed answer.' %} +{%- endif %} +{{- '<|im_start|>system\n' + system_message + '<|im_end|>\n' }} +{%- for message in messages %} + {%- if loop.index0 >= loop_start %} + {{- '<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>\n' }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|im_start|>assistant\n' }} +{%- endif %})TEMPLATE", + }, + // Phi-3.1-mini-128k-instruct-Q4_0.gguf (nomic-ai/gpt4all#3346) + { + // original + R"TEMPLATE({% for message in messages %}{% if message['role'] == 'system' %}{{'<|system|> +' + message['content'] + '<|end|> +'}}{% elif message['role'] == 'user' %}{{'<|user|> +' + message['content'] + '<|end|> +'}}{% elif message['role'] == 'assistant' %}{{'<|assistant|> +' + message['content'] + '<|end|> +'}}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|> +' }}{% else %}{{ eos_token }}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({%- for message in messages %} + {%- if message['role'] == 'system' %} + {{- '<|system|>\n' + message['content'] + '<|end|>\n' }} + {%- elif message['role'] == 'user' %} + {{- '<|user|>\n' + message['content'] + '<|end|>\n' }} + {%- elif message['role'] == 'assistant' %} + {{- '<|assistant|>\n' + message['content'] + '<|end|>\n' }} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|assistant|>\n' }} +{%- else %} + {{- eos_token }} +{%- endif %})TEMPLATE", + }, + // Phi-3-mini-4k-instruct.Q4_0.gguf + { + // original + R"TEMPLATE({{ bos_token }}{% for message in messages %}{{'<|' + message['role'] + '|>' + ' +' + message['content'] + '<|end|> +' }}{% endfor %}{% if add_generation_prompt %}{{ '<|assistant|> +' }}{% else %}{{ eos_token }}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({{- bos_token }} +{%- for message in messages %} + {{- '<|' + message['role'] + '|>\n' + message['content'] + '<|end|>\n' }} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|assistant|>\n' }} +{%- else %} + {{- eos_token }} +{%- endif %})TEMPLATE", + }, + // qwen2-1_5b-instruct-q4_0.gguf (nomic-ai/gpt4all#3263), qwen2-72b-instruct-q4_0.gguf + { + // original + R"TEMPLATE({% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>system +You are a helpful assistant.<|im_end|> +' }}{% endif %}{{'<|im_start|>' + message['role'] + ' +' + message['content'] + '<|im_end|>' + ' +'}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant +' }}{% endif %})TEMPLATE", + // replacement + R"TEMPLATE({%- for message in messages %} + {%- if loop.first and messages[0]['role'] != 'system' %} + {{- '<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n' }} + {%- endif %} + {{- '<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>\n' }} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|im_start|>assistant\n' }} +{%- endif %})TEMPLATE", + }, +}; diff --git a/gpt4all-chat/src/jinja_replacements.h b/gpt4all-chat/src/jinja_replacements.h new file mode 100644 index 000000000000..90f92cba9f34 --- /dev/null +++ b/gpt4all-chat/src/jinja_replacements.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include + +extern const std::unordered_map CHAT_TEMPLATE_SUBSTITUTIONS; diff --git a/gpt4all-chat/src/main.cpp b/gpt4all-chat/src/main.cpp index b07867c6f093..41eb7bb413cb 100644 --- a/gpt4all-chat/src/main.cpp +++ b/gpt4all-chat/src/main.cpp @@ -7,19 +7,32 @@ #include "modellist.h" #include "mysettings.h" #include "network.h" +#include "toolmodel.h" #include #include #include +#include +#include #include #include +#include #include #include #include #include +#include #include +#if G4A_CONFIG(force_d3d12) +# include +#endif + +#ifndef GPT4ALL_USE_QTPDF +# include +#endif + #ifdef Q_OS_LINUX # include #endif @@ -53,6 +66,10 @@ static void raiseWindow(QWindow *window) int main(int argc, char *argv[]) { +#ifndef GPT4ALL_USE_QTPDF + FPDF_InitLibrary(); +#endif + QCoreApplication::setOrganizationName("nomic.ai"); QCoreApplication::setOrganizationDomain("gpt4all.io"); QCoreApplication::setApplicationName("GPT4All"); @@ -70,46 +87,60 @@ int main(int argc, char *argv[]) return 0; } +#if G4A_CONFIG(force_d3d12) + QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D12); +#endif + #ifdef Q_OS_LINUX app.setWindowIcon(QIcon(":/gpt4all/icons/gpt4all.svg")); #endif // set search path before constructing the MySettings instance, which relies on this - QString llmodelSearchPaths = QCoreApplication::applicationDirPath(); - const QString libDir = QCoreApplication::applicationDirPath() + "/../lib/"; - if (LLM::directoryExists(libDir)) - llmodelSearchPaths += ";" + libDir; -#if defined(Q_OS_MAC) - const QString binDir = QCoreApplication::applicationDirPath() + "/../../../"; - if (LLM::directoryExists(binDir)) - llmodelSearchPaths += ";" + binDir; - const QString frameworksDir = QCoreApplication::applicationDirPath() + "/../Frameworks/"; - if (LLM::directoryExists(frameworksDir)) - llmodelSearchPaths += ";" + frameworksDir; + { + auto appDirPath = QCoreApplication::applicationDirPath(); + QStringList searchPaths { +#ifdef Q_OS_DARWIN + u"%1/../Frameworks"_s.arg(appDirPath), +#else + appDirPath, + u"%1/../lib"_s.arg(appDirPath), #endif - LLModel::Implementation::setImplementationsSearchPath(llmodelSearchPaths.toStdString()); + }; + LLModel::Implementation::setImplementationsSearchPath(searchPaths.join(u';').toStdString()); + } // Set the local and language translation before the qml engine has even been started. This will // use the default system locale unless the user has explicitly set it to use a different one. - MySettings::globalInstance()->setLanguageAndLocale(); + auto *mySettings = MySettings::globalInstance(); + mySettings->setLanguageAndLocale(); QQmlApplicationEngine engine; // Add a connection here from MySettings::languageAndLocaleChanged signal to a lambda slot where I can call // engine.uiLanguage property - QObject::connect(MySettings::globalInstance(), &MySettings::languageAndLocaleChanged, [&engine]() { + QObject::connect(mySettings, &MySettings::languageAndLocaleChanged, [&engine]() { engine.setUiLanguage(MySettings::globalInstance()->languageAndLocale()); }); - qmlRegisterSingletonInstance("mysettings", 1, 0, "MySettings", MySettings::globalInstance()); - qmlRegisterSingletonInstance("modellist", 1, 0, "ModelList", ModelList::globalInstance()); + auto *modelList = ModelList::globalInstance(); + QObject::connect(modelList, &ModelList::dataChanged, mySettings, &MySettings::onModelInfoChanged); + + qmlRegisterSingletonInstance("mysettings", 1, 0, "MySettings", mySettings); + qmlRegisterSingletonInstance("modellist", 1, 0, "ModelList", modelList); qmlRegisterSingletonInstance("chatlistmodel", 1, 0, "ChatListModel", ChatListModel::globalInstance()); qmlRegisterSingletonInstance("llm", 1, 0, "LLM", LLM::globalInstance()); qmlRegisterSingletonInstance("download", 1, 0, "Download", Download::globalInstance()); qmlRegisterSingletonInstance("network", 1, 0, "Network", Network::globalInstance()); qmlRegisterSingletonInstance("localdocs", 1, 0, "LocalDocs", LocalDocs::globalInstance()); + qmlRegisterSingletonInstance("toollist", 1, 0, "ToolList", ToolModel::globalInstance()); + qmlRegisterUncreatableMetaObject(ToolEnums::staticMetaObject, "toolenums", 1, 0, "ToolEnums", "Error: only enums"); qmlRegisterUncreatableMetaObject(MySettingsEnums::staticMetaObject, "mysettingsenums", 1, 0, "MySettingsEnums", "Error: only enums"); + { + auto fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); + engine.rootContext()->setContextProperty("fixedFont", fixedFont); + } + const QUrl url(u"qrc:/gpt4all/main.qml"_s); QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, diff --git a/gpt4all-chat/src/modellist.cpp b/gpt4all-chat/src/modellist.cpp index 7cd36094a3e7..d075b770b8c1 100644 --- a/gpt4all-chat/src/modellist.cpp +++ b/gpt4all-chat/src/modellist.cpp @@ -1,6 +1,7 @@ #include "modellist.h" #include "download.h" +#include "jinja_replacements.h" #include "mysettings.h" #include "network.h" @@ -47,8 +48,35 @@ using namespace Qt::Literals::StringLiterals; #define MODELS_JSON_VERSION "3" + static const QStringList FILENAME_BLACKLIST { u"gpt4all-nomic-embed-text-v1.rmodel"_s }; +static const QString RMODEL_CHAT_TEMPLATE = uR"( +{%- set loop_messages = messages %} +{%- for message in loop_messages %} + {%- if not message['role'] in ['user', 'assistant', 'system'] %} + {{- raise_exception('Unknown role: ' + message['role']) }} + {%- endif %} + {{- '<' + message['role'] + '>' }} + {%- if message['role'] == 'user' %} + {%- for source in message.sources %} + {%- if loop.first %} + {{- '### Context:\n' }} + {%- endif %} + {{- ('Collection: ' + source.collection + '\n' + + 'Path: ' + source.path + '\n' + + 'Excerpt: ' + source.text + '\n\n') | escape }} + {%- endfor %} + {%- endif %} + {%- for attachment in message.prompt_attachments %} + {{- (attachment.processed_content + '\n\n') | escape }} + {%- endfor %} + {{- message.content | escape }} + {{- '' }} +{%- endfor %} +)"_s; + + QString ModelInfo::id() const { return m_id; @@ -316,26 +344,55 @@ void ModelInfo::setRepeatPenaltyTokens(int t) m_repeatPenaltyTokens = t; } -QString ModelInfo::promptTemplate() const -{ - return MySettings::globalInstance()->modelPromptTemplate(*this); +QVariant ModelInfo::defaultChatTemplate() const +{ + auto res = m_chatTemplate.or_else([this]() -> std::optional { + if (!installed || isOnline) + return std::nullopt; + if (!m_modelChatTemplate) { + auto path = (dirpath + filename()).toUtf8(); + auto res = LLModel::Implementation::chatTemplate(path.constData()); + if (res) { + std::string ggufTmpl(std::move(*res)); + if (ggufTmpl.size() >= 2 && ggufTmpl.end()[-2] != '\n' && ggufTmpl.end()[-1] == '\n') + ggufTmpl.erase(ggufTmpl.end() - 1); // strip trailing newline for e.g. Llama-3.2-3B-Instruct + if ( + auto replacement = CHAT_TEMPLATE_SUBSTITUTIONS.find(ggufTmpl); + replacement != CHAT_TEMPLATE_SUBSTITUTIONS.end() + ) { + qWarning() << "automatically substituting chat template for" << filename(); + auto &[badTemplate, goodTemplate] = *replacement; + ggufTmpl = goodTemplate; + } + m_modelChatTemplate = QString::fromStdString(ggufTmpl); + } else { + qWarning().nospace() << "failed to get chat template for " << filename() << ": " << res.error().c_str(); + m_modelChatTemplate = QString(); // do not retry + } + } + if (m_modelChatTemplate->isNull()) + return std::nullopt; + return m_modelChatTemplate; + }); + + if (res) + return std::move(*res); + return QVariant::fromValue(nullptr); } -void ModelInfo::setPromptTemplate(const QString &t) +auto ModelInfo::chatTemplate() const -> UpgradeableSetting { - if (shouldSaveMetadata()) MySettings::globalInstance()->setModelPromptTemplate(*this, t, true /*force*/); - m_promptTemplate = t; + return MySettings::globalInstance()->modelChatTemplate(*this); } -QString ModelInfo::systemPrompt() const +QString ModelInfo::defaultSystemMessage() const { - return MySettings::globalInstance()->modelSystemPrompt(*this); + return m_systemMessage; } -void ModelInfo::setSystemPrompt(const QString &p) +auto ModelInfo::systemMessage() const -> UpgradeableSetting { - if (shouldSaveMetadata()) MySettings::globalInstance()->setModelSystemPrompt(*this, p, true /*force*/); - m_systemPrompt = p; + return MySettings::globalInstance()->modelSystemMessage(*this); } QString ModelInfo::chatNamePrompt() const @@ -360,39 +417,41 @@ void ModelInfo::setSuggestedFollowUpPrompt(const QString &p) m_suggestedFollowUpPrompt = p; } +// FIXME(jared): this should not be used for model settings that have meaningful defaults, such as temperature bool ModelInfo::shouldSaveMetadata() const { return installed && (isClone() || isDiscovered() || description() == "" /*indicates sideloaded*/); } -QVariantMap ModelInfo::getFields() const -{ - return { - { "filename", m_filename }, - { "description", m_description }, - { "url", m_url }, - { "quant", m_quant }, - { "type", m_type }, - { "isClone", m_isClone }, - { "isDiscovered", m_isDiscovered }, - { "likes", m_likes }, - { "downloads", m_downloads }, - { "recency", m_recency }, - { "temperature", m_temperature }, - { "topP", m_topP }, - { "minP", m_minP }, - { "topK", m_topK }, - { "maxLength", m_maxLength }, - { "promptBatchSize", m_promptBatchSize }, - { "contextLength", m_contextLength }, - { "gpuLayers", m_gpuLayers }, - { "repeatPenalty", m_repeatPenalty }, - { "repeatPenaltyTokens", m_repeatPenaltyTokens }, - { "promptTemplate", m_promptTemplate }, - { "systemPrompt", m_systemPrompt }, - { "chatNamePrompt", m_chatNamePrompt }, - { "suggestedFollowUpPrompt", m_suggestedFollowUpPrompt }, +QVariant ModelInfo::getField(QLatin1StringView name) const +{ + static const std::unordered_map s_fields = { + { "filename"_L1, [](auto &i) -> QVariant { return i.m_filename; } }, + { "description"_L1, [](auto &i) -> QVariant { return i.m_description; } }, + { "url"_L1, [](auto &i) -> QVariant { return i.m_url; } }, + { "quant"_L1, [](auto &i) -> QVariant { return i.m_quant; } }, + { "type"_L1, [](auto &i) -> QVariant { return i.m_type; } }, + { "isClone"_L1, [](auto &i) -> QVariant { return i.m_isClone; } }, + { "isDiscovered"_L1, [](auto &i) -> QVariant { return i.m_isDiscovered; } }, + { "likes"_L1, [](auto &i) -> QVariant { return i.m_likes; } }, + { "downloads"_L1, [](auto &i) -> QVariant { return i.m_downloads; } }, + { "recency"_L1, [](auto &i) -> QVariant { return i.m_recency; } }, + { "temperature"_L1, [](auto &i) -> QVariant { return i.m_temperature; } }, + { "topP"_L1, [](auto &i) -> QVariant { return i.m_topP; } }, + { "minP"_L1, [](auto &i) -> QVariant { return i.m_minP; } }, + { "topK"_L1, [](auto &i) -> QVariant { return i.m_topK; } }, + { "maxLength"_L1, [](auto &i) -> QVariant { return i.m_maxLength; } }, + { "promptBatchSize"_L1, [](auto &i) -> QVariant { return i.m_promptBatchSize; } }, + { "contextLength"_L1, [](auto &i) -> QVariant { return i.m_contextLength; } }, + { "gpuLayers"_L1, [](auto &i) -> QVariant { return i.m_gpuLayers; } }, + { "repeatPenalty"_L1, [](auto &i) -> QVariant { return i.m_repeatPenalty; } }, + { "repeatPenaltyTokens"_L1, [](auto &i) -> QVariant { return i.m_repeatPenaltyTokens; } }, + { "chatTemplate"_L1, [](auto &i) -> QVariant { return i.defaultChatTemplate(); } }, + { "systemMessage"_L1, [](auto &i) -> QVariant { return i.m_systemMessage; } }, + { "chatNamePrompt"_L1, [](auto &i) -> QVariant { return i.m_chatNamePrompt; } }, + { "suggestedFollowUpPrompt"_L1, [](auto &i) -> QVariant { return i.m_suggestedFollowUpPrompt; } }, }; + return s_fields.at(name)(*this); } InstalledModels::InstalledModels(QObject *parent, bool selectable) @@ -418,47 +477,64 @@ bool InstalledModels::filterAcceptsRow(int sourceRow, return (isInstalled || (!m_selectable && isDownloading)) && !isEmbeddingModel; } -DownloadableModels::DownloadableModels(QObject *parent) +GPT4AllDownloadableModels::GPT4AllDownloadableModels(QObject *parent) : QSortFilterProxyModel(parent) - , m_expanded(false) - , m_limit(5) { - connect(this, &DownloadableModels::rowsInserted, this, &DownloadableModels::countChanged); - connect(this, &DownloadableModels::rowsRemoved, this, &DownloadableModels::countChanged); - connect(this, &DownloadableModels::modelReset, this, &DownloadableModels::countChanged); + connect(this, &GPT4AllDownloadableModels::rowsInserted, this, &GPT4AllDownloadableModels::countChanged); + connect(this, &GPT4AllDownloadableModels::rowsRemoved, this, &GPT4AllDownloadableModels::countChanged); + connect(this, &GPT4AllDownloadableModels::modelReset, this, &GPT4AllDownloadableModels::countChanged); } -bool DownloadableModels::filterAcceptsRow(int sourceRow, +void GPT4AllDownloadableModels::filter(const QVector &keywords) +{ + m_keywords = keywords; + invalidateFilter(); +} + +bool GPT4AllDownloadableModels::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - // FIXME We can eliminate the 'expanded' code as the UI no longer uses this - bool withinLimit = sourceRow < (m_expanded ? sourceModel()->rowCount() : m_limit); QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); - bool hasDescription = !sourceModel()->data(index, ModelList::DescriptionRole).toString().isEmpty(); + const QString description = sourceModel()->data(index, ModelList::DescriptionRole).toString(); + bool hasDescription = !description.isEmpty(); bool isClone = sourceModel()->data(index, ModelList::IsCloneRole).toBool(); - return withinLimit && hasDescription && !isClone; + bool isDiscovered = sourceModel()->data(index, ModelList::IsDiscoveredRole).toBool(); + bool satisfiesKeyword = m_keywords.isEmpty(); + for (const QString &k : m_keywords) + satisfiesKeyword = description.contains(k) ? true : satisfiesKeyword; + return !isDiscovered && hasDescription && !isClone && satisfiesKeyword; } -int DownloadableModels::count() const +int GPT4AllDownloadableModels::count() const { return rowCount(); } -bool DownloadableModels::isExpanded() const +HuggingFaceDownloadableModels::HuggingFaceDownloadableModels(QObject *parent) + : QSortFilterProxyModel(parent) + , m_limit(5) { - return m_expanded; + connect(this, &HuggingFaceDownloadableModels::rowsInserted, this, &HuggingFaceDownloadableModels::countChanged); + connect(this, &HuggingFaceDownloadableModels::rowsRemoved, this, &HuggingFaceDownloadableModels::countChanged); + connect(this, &HuggingFaceDownloadableModels::modelReset, this, &HuggingFaceDownloadableModels::countChanged); } -void DownloadableModels::setExpanded(bool expanded) +bool HuggingFaceDownloadableModels::filterAcceptsRow(int sourceRow, + const QModelIndex &sourceParent) const { - if (m_expanded != expanded) { - m_expanded = expanded; - invalidateFilter(); - emit expandedChanged(m_expanded); - } + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + bool hasDescription = !sourceModel()->data(index, ModelList::DescriptionRole).toString().isEmpty(); + bool isClone = sourceModel()->data(index, ModelList::IsCloneRole).toBool(); + bool isDiscovered = sourceModel()->data(index, ModelList::IsDiscoveredRole).toBool(); + return isDiscovered && hasDescription && !isClone; +} + +int HuggingFaceDownloadableModels::count() const +{ + return rowCount(); } -void DownloadableModels::discoverAndFilter(const QString &discover) +void HuggingFaceDownloadableModels::discoverAndFilter(const QString &discover) { m_discoverFilter = discover; ModelList *ml = qobject_cast(parent()); @@ -476,7 +552,8 @@ ModelList::ModelList() : QAbstractListModel(nullptr) , m_installedModels(new InstalledModels(this)) , m_selectableModels(new InstalledModels(this, /*selectable*/ true)) - , m_downloadableModels(new DownloadableModels(this)) + , m_gpt4AllDownloadableModels(new GPT4AllDownloadableModels(this)) + , m_huggingFaceDownloadableModels(new HuggingFaceDownloadableModels(this)) , m_asyncModelRequestOngoing(false) , m_discoverLimit(20) , m_discoverSortDirection(-1) @@ -489,33 +566,51 @@ ModelList::ModelList() m_installedModels->setSourceModel(this); m_selectableModels->setSourceModel(this); - m_downloadableModels->setSourceModel(this); - - connect(MySettings::globalInstance(), &MySettings::modelPathChanged, this, &ModelList::updateModelsFromDirectory); - connect(MySettings::globalInstance(), &MySettings::modelPathChanged, this, &ModelList::updateModelsFromJson); - connect(MySettings::globalInstance(), &MySettings::modelPathChanged, this, &ModelList::updateModelsFromSettings); - connect(MySettings::globalInstance(), &MySettings::nameChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::temperatureChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::topPChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::minPChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::topKChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::maxLengthChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::promptBatchSizeChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::contextLengthChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::gpuLayersChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::repeatPenaltyChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::repeatPenaltyTokensChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::promptTemplateChanged, this, &ModelList::updateDataForSettings); - connect(MySettings::globalInstance(), &MySettings::systemPromptChanged, this, &ModelList::updateDataForSettings); + m_gpt4AllDownloadableModels->setSourceModel(this); + m_huggingFaceDownloadableModels->setSourceModel(this); + + auto *mySettings = MySettings::globalInstance(); + connect(mySettings, &MySettings::nameChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::temperatureChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::topPChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::minPChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::topKChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::maxLengthChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::promptBatchSizeChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::contextLengthChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::gpuLayersChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::repeatPenaltyChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::repeatPenaltyTokensChanged, this, &ModelList::updateDataForSettings ); + connect(mySettings, &MySettings::chatTemplateChanged, this, &ModelList::maybeUpdateDataForSettings); + connect(mySettings, &MySettings::systemMessageChanged, this, &ModelList::maybeUpdateDataForSettings); + + connect(this, &ModelList::dataChanged, this, &ModelList::onDataChanged); + connect(&m_networkManager, &QNetworkAccessManager::sslErrors, this, &ModelList::handleSslErrors); updateModelsFromJson(); updateModelsFromSettings(); updateModelsFromDirectory(); + connect(mySettings, &MySettings::modelPathChanged, this, &ModelList::updateModelsFromDirectory); + connect(mySettings, &MySettings::modelPathChanged, this, &ModelList::updateModelsFromJson ); + connect(mySettings, &MySettings::modelPathChanged, this, &ModelList::updateModelsFromSettings ); + QCoreApplication::instance()->installEventFilter(this); } +// an easier way to listen for model info and setting changes +void ModelList::onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles) +{ + Q_UNUSED(roles) + for (int row = topLeft.row(); row <= bottomRight.row(); row++) { + auto index = topLeft.siblingAtRow(row); + auto id = index.data(ModelList::IdRole).toString(); + if (auto info = modelInfo(id); !info.id().isNull()) + emit modelInfoChanged(info); + } +} + QString ModelList::compatibleModelNameHash(QUrl baseUrl, QString modelName) { QCryptographicHash sha256(QCryptographicHash::Sha256); sha256.addData((baseUrl.toString() + "_" + modelName).toUtf8()); @@ -776,10 +871,10 @@ QVariant ModelList::dataInternal(const ModelInfo *info, int role) const return info->repeatPenalty(); case RepeatPenaltyTokensRole: return info->repeatPenaltyTokens(); - case PromptTemplateRole: - return info->promptTemplate(); - case SystemPromptRole: - return info->systemPrompt(); + case ChatTemplateRole: + return QVariant::fromValue(info->chatTemplate()); + case SystemMessageRole: + return QVariant::fromValue(info->systemMessage()); case ChatNamePromptRole: return info->chatNamePrompt(); case SuggestedFollowUpPromptRole: @@ -952,10 +1047,10 @@ void ModelList::updateData(const QString &id, const QVector info->setRepeatPenalty(value.toDouble()); break; case RepeatPenaltyTokensRole: info->setRepeatPenaltyTokens(value.toInt()); break; - case PromptTemplateRole: - info->setPromptTemplate(value.toString()); break; - case SystemPromptRole: - info->setSystemPrompt(value.toString()); break; + case ChatTemplateRole: + info->m_chatTemplate = value.toString(); break; + case SystemMessageRole: + info->m_systemMessage = value.toString(); break; case ChatNamePromptRole: info->setChatNamePrompt(value.toString()); break; case SuggestedFollowUpPromptRole: @@ -1056,11 +1151,11 @@ ModelInfo ModelList::modelInfo(const QString &id) const return *m_modelMap.value(id); } -ModelInfo ModelList::modelInfoByFilename(const QString &filename) const +ModelInfo ModelList::modelInfoByFilename(const QString &filename, bool allowClone) const { QMutexLocker locker(&m_mutex); for (ModelInfo *info : m_models) - if (info->filename() == filename) + if (info->filename() == filename && (allowClone || !info->isClone())) return *info; return ModelInfo(); } @@ -1077,9 +1172,25 @@ bool ModelList::isUniqueName(const QString &name) const QString ModelList::clone(const ModelInfo &model) { + auto *mySettings = MySettings::globalInstance(); + const QString id = Network::globalInstance()->generateUniqueId(); addModel(id); + QString tmplSetting, sysmsgSetting; + if (auto tmpl = model.chatTemplate().asModern()) { + tmplSetting = *tmpl; + } else { + qWarning("ModelList Warning: attempted to clone model with legacy chat template"); + return {}; + } + if (auto msg = model.systemMessage().asModern()) { + sysmsgSetting = *msg; + } else { + qWarning("ModelList Warning: attempted to clone model with legacy system message"); + return {}; + } + QVector> data { { ModelList::InstalledRole, model.installed }, { ModelList::IsCloneRole, true }, @@ -1099,12 +1210,22 @@ QString ModelList::clone(const ModelInfo &model) { ModelList::GpuLayersRole, model.gpuLayers() }, { ModelList::RepeatPenaltyRole, model.repeatPenalty() }, { ModelList::RepeatPenaltyTokensRole, model.repeatPenaltyTokens() }, - { ModelList::PromptTemplateRole, model.promptTemplate() }, - { ModelList::SystemPromptRole, model.systemPrompt() }, + { ModelList::SystemMessageRole, model.m_systemMessage }, { ModelList::ChatNamePromptRole, model.chatNamePrompt() }, { ModelList::SuggestedFollowUpPromptRole, model.suggestedFollowUpPrompt() }, }; + if (auto tmpl = model.m_chatTemplate) + data.emplace_back(ModelList::ChatTemplateRole, *tmpl); // copy default chat template, if known updateData(id, data); + + // Ensure setting overrides are copied in case the base model overrides change. + // This is necessary because setting these roles on ModelInfo above does not write to settings. + auto cloneInfo = modelInfo(id); + if (mySettings->isModelChatTemplateSet (model)) + mySettings->setModelChatTemplate (cloneInfo, tmplSetting ); + if (mySettings->isModelSystemMessageSet(model)) + mySettings->setModelSystemMessage(cloneInfo, sysmsgSetting); + return id; } @@ -1125,21 +1246,23 @@ void ModelList::removeInstalled(const ModelInfo &model) removeInternal(model); } +int ModelList::indexByModelId(const QString &id) const +{ + QMutexLocker locker(&m_mutex); + if (auto it = m_modelMap.find(id); it != m_modelMap.cend()) + return m_models.indexOf(*it); + return -1; +} + void ModelList::removeInternal(const ModelInfo &model) { - const bool hasModel = contains(model.id()); - Q_ASSERT(hasModel); - if (!hasModel) { + int indexOfModel = indexByModelId(model.id()); + Q_ASSERT(indexOfModel != -1); + if (indexOfModel == -1) { qWarning() << "ERROR: model list does not contain" << model.id(); return; } - int indexOfModel = 0; - { - QMutexLocker locker(&m_mutex); - ModelInfo *info = m_modelMap.value(model.id()); - indexOfModel = m_models.indexOf(info); - } beginRemoveRows(QModelIndex(), indexOfModel, indexOfModel); { QMutexLocker locker(&m_mutex); @@ -1314,8 +1437,7 @@ void ModelList::processModelDirectory(const QString &path) // The description is hard-coded into "GPT4All.ini" due to performance issue. // If the description goes to be dynamic from its .rmodel file, it will get high I/O usage while using the ModelList. data.append({ DescriptionRole, description }); - // Prompt template should be clear while using ChatML format which is using in most of OpenAI-Compatible API server. - data.append({ PromptTemplateRole, "%1" }); + data.append({ ChatTemplateRole, RMODEL_CHAT_TEMPLATE }); } updateData(id, data); } @@ -1451,9 +1573,20 @@ void ModelList::handleSslErrors(QNetworkReply *reply, const QList &er qWarning() << "ERROR: Received ssl error:" << e.errorString() << "for" << url; } +void ModelList::maybeUpdateDataForSettings(const ModelInfo &info, bool fromInfo) +{ + // ignore updates that were *because* of a dataChanged - would cause a circular dependency + int idx; + if (!fromInfo && (idx = indexByModelId(info.id())) != -1) { + emit dataChanged(index(idx, 0), index(idx, 0)); + emit selectableModelListChanged(); + } +} + void ModelList::updateDataForSettings() { emit dataChanged(index(0, 0), index(m_models.size() - 1, 0)); + emit selectableModelListChanged(); } void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) @@ -1488,7 +1621,6 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) QString requiresVersion = obj["requires"].toString(); QString versionRemoved = obj["removedIn"].toString(); QString url = obj["url"].toString(); - QByteArray modelHash = obj["md5sum"].toString().toLatin1(); bool isDefault = obj.contains("isDefault") && obj["isDefault"] == u"true"_s; bool disableGUI = obj.contains("disableGUI") && obj["disableGUI"] == u"true"_s; QString description = obj["description"].toString(); @@ -1499,6 +1631,16 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) QString type = obj["type"].toString(); bool isEmbeddingModel = obj["embeddingModel"].toBool(); + QByteArray modelHash; + ModelInfo::HashAlgorithm hashAlgorithm; + if (auto it = obj.find("sha256sum"_L1); it != obj.end()) { + modelHash = it->toString().toLatin1(); + hashAlgorithm = ModelInfo::Sha256; + } else { + modelHash = obj["md5sum"].toString().toLatin1(); + hashAlgorithm = ModelInfo::Md5; + } + // Some models aren't supported in the GUI at all if (disableGUI) continue; @@ -1527,7 +1669,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) { ModelList::FilenameRole, modelFilename }, { ModelList::FilesizeRole, modelFilesize }, { ModelList::HashRole, modelHash }, - { ModelList::HashAlgorithmRole, ModelInfo::Md5 }, + { ModelList::HashAlgorithmRole, hashAlgorithm }, { ModelList::DefaultRole, isDefault }, { ModelList::DescriptionRole, description }, { ModelList::RequiresVersionRole, requiresVersion }, @@ -1560,10 +1702,10 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) data.append({ ModelList::RepeatPenaltyRole, obj["repeatPenalty"].toDouble() }); if (obj.contains("repeatPenaltyTokens")) data.append({ ModelList::RepeatPenaltyTokensRole, obj["repeatPenaltyTokens"].toInt() }); - if (obj.contains("promptTemplate")) - data.append({ ModelList::PromptTemplateRole, obj["promptTemplate"].toString() }); - if (obj.contains("systemPrompt")) - data.append({ ModelList::SystemPromptRole, obj["systemPrompt"].toString() }); + if (auto it = obj.find("chatTemplate"_L1); it != obj.end()) + data.append({ ModelList::ChatTemplateRole, it->toString() }); + if (auto it = obj.find("systemMessage"_L1); it != obj.end()) + data.append({ ModelList::SystemMessageRole, it->toString() }); updateData(id, data); } @@ -1593,7 +1735,8 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) { ModelList::ParametersRole, "?" }, { ModelList::QuantRole, "NA" }, { ModelList::TypeRole, "GPT" }, - { ModelList::UrlRole, "https://api.openai.com/v1/chat/completions"}, + { ModelList::UrlRole, "https://api.openai.com/v1/chat/completions" }, + { ModelList::ChatTemplateRole, RMODEL_CHAT_TEMPLATE }, }; updateData(id, data); } @@ -1621,7 +1764,8 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) { ModelList::ParametersRole, "?" }, { ModelList::QuantRole, "NA" }, { ModelList::TypeRole, "GPT" }, - { ModelList::UrlRole, "https://api.openai.com/v1/chat/completions"}, + { ModelList::UrlRole, "https://api.openai.com/v1/chat/completions" }, + { ModelList::ChatTemplateRole, RMODEL_CHAT_TEMPLATE }, }; updateData(id, data); } @@ -1652,7 +1796,8 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) { ModelList::ParametersRole, "?" }, { ModelList::QuantRole, "NA" }, { ModelList::TypeRole, "Mistral" }, - { ModelList::UrlRole, "https://api.mistral.ai/v1/chat/completions"}, + { ModelList::UrlRole, "https://api.mistral.ai/v1/chat/completions" }, + { ModelList::ChatTemplateRole, RMODEL_CHAT_TEMPLATE }, }; updateData(id, data); } @@ -1677,7 +1822,8 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) { ModelList::ParametersRole, "?" }, { ModelList::QuantRole, "NA" }, { ModelList::TypeRole, "Mistral" }, - { ModelList::UrlRole, "https://api.mistral.ai/v1/chat/completions"}, + { ModelList::UrlRole, "https://api.mistral.ai/v1/chat/completions" }, + { ModelList::ChatTemplateRole, RMODEL_CHAT_TEMPLATE }, }; updateData(id, data); } @@ -1703,7 +1849,8 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) { ModelList::ParametersRole, "?" }, { ModelList::QuantRole, "NA" }, { ModelList::TypeRole, "Mistral" }, - { ModelList::UrlRole, "https://api.mistral.ai/v1/chat/completions"}, + { ModelList::UrlRole, "https://api.mistral.ai/v1/chat/completions" }, + { ModelList::ChatTemplateRole, RMODEL_CHAT_TEMPLATE }, }; updateData(id, data); } @@ -1732,6 +1879,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save) { ModelList::ParametersRole, "?" }, { ModelList::QuantRole, "NA" }, { ModelList::TypeRole, "NA" }, + { ModelList::ChatTemplateRole, RMODEL_CHAT_TEMPLATE }, }; updateData(id, data); } @@ -1755,6 +1903,9 @@ void ModelList::updateDiscoveredInstalled(const ModelInfo &info) updateData(info.id(), data); } +// FIXME(jared): This should only contain fields without reasonable defaults such as name, description, and URL. +// For other settings, there is no authoritative value and we should load the setting lazily like we do +// for any other override. void ModelList::updateModelsFromSettings() { QSettings settings; @@ -1769,12 +1920,27 @@ void ModelList::updateModelsFromSettings() // If we can't find the corresponding file, then ignore it as this reflects a stale model. // The file could have been deleted manually by the user for instance or temporarily renamed. - if (!settings.contains(g + "/filename") || !modelExists(settings.value(g + "/filename").toString())) - continue; + QString filename; + { + auto value = settings.value(u"%1/filename"_s.arg(g)); + if (!value.isValid() || !modelExists(filename = value.toString())) + continue; + } + + QVector> data; + + // load data from base model + // FIXME(jared): how does "Restore Defaults" work for other settings of clones which we don't do this for? + if (auto base = modelInfoByFilename(filename, /*allowClone*/ false); !base.id().isNull()) { + if (auto tmpl = base.m_chatTemplate) + data.append({ ModelList::ChatTemplateRole, *tmpl }); + if (auto msg = base.m_systemMessage; !msg.isNull()) + data.append({ ModelList::SystemMessageRole, msg }); + } addModel(id); - QVector> data; + // load data from settings if (settings.contains(g + "/name")) { const QString name = settings.value(g + "/name").toString(); data.append({ ModelList::NameRole, name }); @@ -1859,14 +2025,6 @@ void ModelList::updateModelsFromSettings() const int repeatPenaltyTokens = settings.value(g + "/repeatPenaltyTokens").toInt(); data.append({ ModelList::RepeatPenaltyTokensRole, repeatPenaltyTokens }); } - if (settings.contains(g + "/promptTemplate")) { - const QString promptTemplate = settings.value(g + "/promptTemplate").toString(); - data.append({ ModelList::PromptTemplateRole, promptTemplate }); - } - if (settings.contains(g + "/systemPrompt")) { - const QString systemPrompt = settings.value(g + "/systemPrompt").toString(); - data.append({ ModelList::SystemPromptRole, systemPrompt }); - } if (settings.contains(g + "/chatNamePrompt")) { const QString chatNamePrompt = settings.value(g + "/chatNamePrompt").toString(); data.append({ ModelList::ChatNamePromptRole, chatNamePrompt }); diff --git a/gpt4all-chat/src/modellist.h b/gpt4all-chat/src/modellist.h index 6123dde81b6c..0bcc97b484ee 100644 --- a/gpt4all-chat/src/modellist.h +++ b/gpt4all-chat/src/modellist.h @@ -5,12 +5,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -19,11 +21,53 @@ #include #include +#include #include using namespace Qt::Literals::StringLiterals; +class UpgradeableSetting { + Q_GADGET + QML_ANONYMOUS + + // NOTE: Unset implies there is neither a value nor a default + enum class State { Unset, Legacy, Modern }; + + Q_PROPERTY(bool isSet READ isSet ) + Q_PROPERTY(bool isLegacy READ isLegacy) + Q_PROPERTY(bool isModern READ isModern) + Q_PROPERTY(QVariant value READ value) // string or null + +public: + struct legacy_tag_t { explicit legacy_tag_t() = default; }; + static inline constexpr legacy_tag_t legacy_tag = legacy_tag_t(); + + UpgradeableSetting() : m_state(State::Unset ) {} + UpgradeableSetting(legacy_tag_t, QString value): m_state(State::Legacy), m_value(std::move(value)) {} + UpgradeableSetting( QString value): m_state(State::Modern), m_value(std::move(value)) {} + + bool isSet () const { return m_state != State::Unset; } + bool isLegacy() const { return m_state == State::Legacy; } + bool isModern() const { return m_state == State::Modern; } + QVariant value () const { return m_state == State::Unset ? QVariant::fromValue(nullptr) : m_value; } + + friend bool operator==(const UpgradeableSetting &a, const UpgradeableSetting &b) + { return a.m_state == b.m_state && (a.m_state == State::Unset || a.m_value == b.m_value); } + + // returns std::nullopt if there is a legacy template or it is not set + std::optional asModern() const + { + if (m_state == State::Modern) + return m_value; + return std::nullopt; + } + +private: + State m_state; + QString m_value; +}; + struct ModelInfo { Q_GADGET Q_PROPERTY(QString id READ id WRITE setId) @@ -69,8 +113,11 @@ struct ModelInfo { Q_PROPERTY(int maxGpuLayers READ maxGpuLayers) Q_PROPERTY(double repeatPenalty READ repeatPenalty WRITE setRepeatPenalty) Q_PROPERTY(int repeatPenaltyTokens READ repeatPenaltyTokens WRITE setRepeatPenaltyTokens) - Q_PROPERTY(QString promptTemplate READ promptTemplate WRITE setPromptTemplate) - Q_PROPERTY(QString systemPrompt READ systemPrompt WRITE setSystemPrompt) + // user-defined chat template and system message must be written through settings because of their legacy compat + Q_PROPERTY(QVariant defaultChatTemplate READ defaultChatTemplate ) + Q_PROPERTY(UpgradeableSetting chatTemplate READ chatTemplate ) + Q_PROPERTY(QString defaultSystemMessage READ defaultSystemMessage) + Q_PROPERTY(UpgradeableSetting systemMessage READ systemMessage ) Q_PROPERTY(QString chatNamePrompt READ chatNamePrompt WRITE setChatNamePrompt) Q_PROPERTY(QString suggestedFollowUpPrompt READ suggestedFollowUpPrompt WRITE setSuggestedFollowUpPrompt) Q_PROPERTY(int likes READ likes WRITE setLikes) @@ -178,19 +225,22 @@ struct ModelInfo { void setRepeatPenalty(double p); int repeatPenaltyTokens() const; void setRepeatPenaltyTokens(int t); - QString promptTemplate() const; - void setPromptTemplate(const QString &t); - QString systemPrompt() const; - void setSystemPrompt(const QString &p); + QVariant defaultChatTemplate() const; + UpgradeableSetting chatTemplate() const; + QString defaultSystemMessage() const; + UpgradeableSetting systemMessage() const; QString chatNamePrompt() const; void setChatNamePrompt(const QString &p); QString suggestedFollowUpPrompt() const; void setSuggestedFollowUpPrompt(const QString &p); + // Some metadata must be saved to settings because it does not have a meaningful default from some other source. + // This is useful for fields such as name, description, and URL. + // It is true for any models that have not been installed from models.json. bool shouldSaveMetadata() const; private: - QVariantMap getFields() const; + QVariant getField(QLatin1StringView name) const; QString m_id; QString m_name; @@ -216,11 +266,13 @@ struct ModelInfo { mutable int m_maxGpuLayers = -1; double m_repeatPenalty = 1.18; int m_repeatPenaltyTokens = 64; - QString m_promptTemplate = "### Human:\n%1\n\n### Assistant:\n"; - QString m_systemPrompt = "### System:\nYou are an AI assistant who gives a quality response to whatever humans ask of you.\n\n"; + std::optional m_chatTemplate; + mutable std::optional m_modelChatTemplate; + QString m_systemMessage; QString m_chatNamePrompt = "Describe the above conversation in seven words or less."; QString m_suggestedFollowUpPrompt = "Suggest three very short factual follow-up questions that have not been answered yet or cannot be found inspired by the previous conversation and excerpts."; friend class MySettings; + friend class ModelList; }; Q_DECLARE_METATYPE(ModelInfo) @@ -242,19 +294,15 @@ class InstalledModels : public QSortFilterProxyModel bool m_selectable; }; -class DownloadableModels : public QSortFilterProxyModel +class GPT4AllDownloadableModels : public QSortFilterProxyModel { Q_OBJECT Q_PROPERTY(int count READ count NOTIFY countChanged) - Q_PROPERTY(bool expanded READ isExpanded WRITE setExpanded NOTIFY expandedChanged) public: - explicit DownloadableModels(QObject *parent); + explicit GPT4AllDownloadableModels(QObject *parent); int count() const; - bool isExpanded() const; - void setExpanded(bool expanded); - - Q_INVOKABLE void discoverAndFilter(const QString &discover); + Q_INVOKABLE void filter(const QVector &keywords); Q_SIGNALS: void countChanged(); @@ -262,11 +310,27 @@ class DownloadableModels : public QSortFilterProxyModel protected: bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; +private: + QVector m_keywords; +}; + +class HuggingFaceDownloadableModels : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) +public: + explicit HuggingFaceDownloadableModels(QObject *parent); + int count() const; + + Q_INVOKABLE void discoverAndFilter(const QString &discover); + Q_SIGNALS: - void expandedChanged(bool expanded); + void countChanged(); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; private: - bool m_expanded; int m_limit; QString m_discoverFilter; }; @@ -277,7 +341,8 @@ class ModelList : public QAbstractListModel Q_PROPERTY(int count READ count NOTIFY countChanged) Q_PROPERTY(InstalledModels* installedModels READ installedModels NOTIFY installedModelsChanged) Q_PROPERTY(InstalledModels* selectableModels READ selectableModels NOTIFY selectableModelsChanged) - Q_PROPERTY(DownloadableModels* downloadableModels READ downloadableModels NOTIFY downloadableModelsChanged) + Q_PROPERTY(GPT4AllDownloadableModels* gpt4AllDownloadableModels READ gpt4AllDownloadableModels CONSTANT) + Q_PROPERTY(HuggingFaceDownloadableModels* huggingFaceDownloadableModels READ huggingFaceDownloadableModels CONSTANT) Q_PROPERTY(QList selectableModelList READ selectableModelList NOTIFY selectableModelListChanged) Q_PROPERTY(bool asyncModelRequestOngoing READ asyncModelRequestOngoing NOTIFY asyncModelRequestOngoingChanged) Q_PROPERTY(int discoverLimit READ discoverLimit WRITE setDiscoverLimit NOTIFY discoverLimitChanged) @@ -340,8 +405,8 @@ class ModelList : public QAbstractListModel GpuLayersRole, RepeatPenaltyRole, RepeatPenaltyTokensRole, - PromptTemplateRole, - SystemPromptRole, + ChatTemplateRole, + SystemMessageRole, ChatNamePromptRole, SuggestedFollowUpPromptRole, MinPRole, @@ -394,8 +459,8 @@ class ModelList : public QAbstractListModel roles[GpuLayersRole] = "gpuLayers"; roles[RepeatPenaltyRole] = "repeatPenalty"; roles[RepeatPenaltyTokensRole] = "repeatPenaltyTokens"; - roles[PromptTemplateRole] = "promptTemplate"; - roles[SystemPromptRole] = "systemPrompt"; + roles[ChatTemplateRole] = "chatTemplate"; + roles[SystemMessageRole] = "systemMessage"; roles[ChatNamePromptRole] = "chatNamePrompt"; roles[SuggestedFollowUpPromptRole] = "suggestedFollowUpPrompt"; roles[LikesRole] = "likes"; @@ -416,7 +481,7 @@ class ModelList : public QAbstractListModel bool contains(const QString &id) const; bool containsByFilename(const QString &filename) const; Q_INVOKABLE ModelInfo modelInfo(const QString &id) const; - Q_INVOKABLE ModelInfo modelInfoByFilename(const QString &filename) const; + Q_INVOKABLE ModelInfo modelInfoByFilename(const QString &filename, bool allowClone = true) const; Q_INVOKABLE bool isUniqueName(const QString &name) const; Q_INVOKABLE QString clone(const ModelInfo &model); Q_INVOKABLE void removeClone(const ModelInfo &model); @@ -430,7 +495,8 @@ class ModelList : public QAbstractListModel InstalledModels *installedModels() const { return m_installedModels; } InstalledModels *selectableModels() const { return m_selectableModels; } - DownloadableModels *downloadableModels() const { return m_downloadableModels; } + GPT4AllDownloadableModels *gpt4AllDownloadableModels() const { return m_gpt4AllDownloadableModels; } + HuggingFaceDownloadableModels *huggingFaceDownloadableModels() const { return m_huggingFaceDownloadableModels; } static inline QString toFileSize(quint64 sz) { if (sz < 1024) { @@ -468,7 +534,6 @@ class ModelList : public QAbstractListModel void countChanged(); void installedModelsChanged(); void selectableModelsChanged(); - void downloadableModelsChanged(); void selectableModelListChanged(); void asyncModelRequestOngoingChanged(); void discoverLimitChanged(); @@ -476,15 +541,18 @@ class ModelList : public QAbstractListModel void discoverSortChanged(); void discoverProgressChanged(); void discoverInProgressChanged(); + void modelInfoChanged(const ModelInfo &info); protected: bool eventFilter(QObject *obj, QEvent *ev) override; private Q_SLOTS: + void onDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles); void resortModel(); void updateModelsFromJson(); void updateModelsFromJsonAsync(); void updateModelsFromSettings(); + void maybeUpdateDataForSettings(const ModelInfo &info, bool fromInfo); void updateDataForSettings(); void handleModelsJsonDownloadFinished(); void handleModelsJsonDownloadErrorOccurred(QNetworkReply::NetworkError code); @@ -495,6 +563,9 @@ private Q_SLOTS: void handleSslErrors(QNetworkReply *reply, const QList &errors); private: + // Return the index of the model with the given id, or -1 if not found. + int indexByModelId(const QString &id) const; + void removeInternal(const ModelInfo &model); void clearDiscoveredModels(); bool modelExists(const QString &fileName) const; @@ -512,7 +583,8 @@ private Q_SLOTS: QNetworkAccessManager m_networkManager; InstalledModels *m_installedModels; InstalledModels *m_selectableModels; - DownloadableModels *m_downloadableModels; + GPT4AllDownloadableModels *m_gpt4AllDownloadableModels; + HuggingFaceDownloadableModels *m_huggingFaceDownloadableModels; QList m_models; QHash m_modelMap; bool m_asyncModelRequestOngoing; diff --git a/gpt4all-chat/src/mysettings.cpp b/gpt4all-chat/src/mysettings.cpp index 87abcc4601ba..ffccc912dade 100644 --- a/gpt4all-chat/src/mysettings.cpp +++ b/gpt4all-chat/src/mysettings.cpp @@ -1,5 +1,8 @@ #include "mysettings.h" +#include "chatllm.h" +#include "modellist.h" + #include #include @@ -29,8 +32,13 @@ static const QStringList suggestionModeNames { "LocalDocsOnly", "On", "Off" }; static const QStringList chatThemeNames { "Light", "Dark", "LegacyDark" }; static const QStringList fontSizeNames { "Small", "Medium", "Large" }; -// FIXME: All of these default strings that are shown in the UI for settings need to be marked as -// translatable +// psuedo-enum +namespace ModelSettingsKey { namespace { + auto ChatTemplate = "chatTemplate"_L1; + auto PromptTemplate = "promptTemplate"_L1; // legacy + auto SystemMessage = "systemMessage"_L1; + auto SystemPrompt = "systemPrompt"_L1; // legacy +} } // namespace ModelSettingsKey::(anonymous) namespace defaults { @@ -48,7 +56,6 @@ static const QVariantMap basicDefaults { { "fontSize", QVariant::fromValue(FontSize::Small) }, { "lastVersionStarted", "" }, { "networkPort", 4891, }, - { "saveChatsContext", false }, { "systemTray", false }, { "serverChat", false }, { "userDefaultModel", "Application default" }, @@ -147,6 +154,11 @@ static QStringList getUiLanguages(const QString &modelPath) return languageList; } +static QString modelSettingName(const ModelInfo &info, auto &&name) +{ + return u"model-%1/%2"_s.arg(info.id(), name); +} + class MyPrivateSettings: public MySettings { }; Q_GLOBAL_STATIC(MyPrivateSettings, settingsInstance) MySettings *MySettings::globalInstance() @@ -162,6 +174,34 @@ MySettings::MySettings() { } +QVariant MySettings::checkJinjaTemplateError(const QString &tmpl) +{ + if (auto err = ChatLLM::checkJinjaTemplateError(tmpl.toStdString())) + return QString::fromStdString(*err); + return QVariant::fromValue(nullptr); +} + +// Unset settings come from ModelInfo. Listen for changes so we can emit our own setting-specific signals. +void MySettings::onModelInfoChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles) +{ + auto settingChanged = [&](const auto &info, auto role, const auto &name) { + return (roles.isEmpty() || roles.contains(role)) && !m_settings.contains(modelSettingName(info, name)); + }; + + auto &modelList = dynamic_cast(*QObject::sender()); + for (int row = topLeft.row(); row <= bottomRight.row(); row++) { + using enum ModelList::Roles; + using namespace ModelSettingsKey; + auto index = topLeft.siblingAtRow(row); + if (auto info = modelList.modelInfo(index.data(IdRole).toString()); !info.id().isNull()) { + if (settingChanged(info, ChatTemplateRole, ChatTemplate)) + emit chatTemplateChanged(info, /*fromInfo*/ true); + if (settingChanged(info, SystemMessageRole, SystemMessage)) + emit systemMessageChanged(info, /*fromInfo*/ true); + } + } +} + QVariant MySettings::getBasicSetting(const QString &name) const { return m_settings.value(name, basicDefaults.value(name)); @@ -194,8 +234,8 @@ void MySettings::restoreModelDefaults(const ModelInfo &info) setModelGpuLayers(info, info.m_gpuLayers); setModelRepeatPenalty(info, info.m_repeatPenalty); setModelRepeatPenaltyTokens(info, info.m_repeatPenaltyTokens); - setModelPromptTemplate(info, info.m_promptTemplate); - setModelSystemPrompt(info, info.m_systemPrompt); + resetModelChatTemplate (info); + resetModelSystemMessage(info); setModelChatNamePrompt(info, info.m_chatNamePrompt); setModelSuggestedFollowUpPrompt(info, info.m_suggestedFollowUpPrompt); } @@ -206,7 +246,6 @@ void MySettings::restoreApplicationDefaults() setFontSize(basicDefaults.value("fontSize").value()); setDevice(defaults::device); setThreadCount(defaults::threadCount); - setSaveChatsContext(basicDefaults.value("saveChatsContext").toBool()); setSystemTray(basicDefaults.value("systemTray").toBool()); setServerChat(basicDefaults.value("serverChat").toBool()); setNetworkPort(basicDefaults.value("networkPort").toInt()); @@ -252,29 +291,37 @@ void MySettings::setModelName(const ModelInfo &info, const QString &value, bool emit nameChanged(info); } -static QString modelSettingName(const ModelInfo &info, const QString &name) +QVariant MySettings::getModelSetting(QLatin1StringView name, const ModelInfo &info) const { - return u"model-%1/%2"_s.arg(info.id(), name); + QLatin1StringView nameL1(name); + return m_settings.value(modelSettingName(info, nameL1), info.getField(nameL1)); } -QVariant MySettings::getModelSetting(const QString &name, const ModelInfo &info) const +QVariant MySettings::getModelSetting(const char *name, const ModelInfo &info) const { - return m_settings.value(modelSettingName(info, name), info.getFields().value(name)); + return getModelSetting(QLatin1StringView(name), info); } -void MySettings::setModelSetting(const QString &name, const ModelInfo &info, const QVariant &value, bool force, +void MySettings::setModelSetting(QLatin1StringView name, const ModelInfo &info, const QVariant &value, bool force, bool signal) { if (!force && (info.id().isEmpty() || getModelSetting(name, info) == value)) return; - QString settingName = modelSettingName(info, name); - if (info.getFields().value(name) == value && !info.shouldSaveMetadata()) + QLatin1StringView nameL1(name); + QString settingName = modelSettingName(info, nameL1); + if (info.getField(nameL1) == value && !info.shouldSaveMetadata()) m_settings.remove(settingName); else m_settings.setValue(settingName, value); if (signal && !force) - QMetaObject::invokeMethod(this, u"%1Changed"_s.arg(name).toLatin1().constData(), Q_ARG(ModelInfo, info)); + QMetaObject::invokeMethod(this, u"%1Changed"_s.arg(nameL1).toLatin1().constData(), Q_ARG(ModelInfo, info)); +} + +void MySettings::setModelSetting(const char *name, const ModelInfo &info, const QVariant &value, bool force, + bool signal) +{ + setModelSetting(QLatin1StringView(name), info, value, force, signal); } QString MySettings::modelFilename (const ModelInfo &info) const { return getModelSetting("filename", info).toString(); } @@ -297,11 +344,68 @@ int MySettings::modelContextLength (const ModelInfo &info) const int MySettings::modelGpuLayers (const ModelInfo &info) const { return getModelSetting("gpuLayers", info).toInt(); } double MySettings::modelRepeatPenalty (const ModelInfo &info) const { return getModelSetting("repeatPenalty", info).toDouble(); } int MySettings::modelRepeatPenaltyTokens (const ModelInfo &info) const { return getModelSetting("repeatPenaltyTokens", info).toInt(); } -QString MySettings::modelPromptTemplate (const ModelInfo &info) const { return getModelSetting("promptTemplate", info).toString(); } -QString MySettings::modelSystemPrompt (const ModelInfo &info) const { return getModelSetting("systemPrompt", info).toString(); } QString MySettings::modelChatNamePrompt (const ModelInfo &info) const { return getModelSetting("chatNamePrompt", info).toString(); } QString MySettings::modelSuggestedFollowUpPrompt(const ModelInfo &info) const { return getModelSetting("suggestedFollowUpPrompt", info).toString(); } +auto MySettings::getUpgradeableModelSetting( + const ModelInfo &info, QLatin1StringView legacyKey, QLatin1StringView newKey +) const -> UpgradeableSetting +{ + if (info.id().isEmpty()) { + qWarning("%s: got null model", Q_FUNC_INFO); + return {}; + } + + auto value = m_settings.value(modelSettingName(info, legacyKey)); + if (value.isValid()) + return { UpgradeableSetting::legacy_tag, value.toString() }; + + value = getModelSetting(newKey, info); + if (!value.isNull()) + return value.toString(); + return {}; // neither a default nor an override +} + +bool MySettings::isUpgradeableModelSettingSet( + const ModelInfo &info, QLatin1StringView legacyKey, QLatin1StringView newKey +) const +{ + if (info.id().isEmpty()) { + qWarning("%s: got null model", Q_FUNC_INFO); + return false; + } + + if (m_settings.contains(modelSettingName(info, legacyKey))) + return true; + + // NOTE: unlike getUpgradeableSetting(), this ignores the default + return m_settings.contains(modelSettingName(info, newKey)); +} + +auto MySettings::modelChatTemplate(const ModelInfo &info) const -> UpgradeableSetting +{ + using namespace ModelSettingsKey; + return getUpgradeableModelSetting(info, PromptTemplate, ChatTemplate); +} + +bool MySettings::isModelChatTemplateSet(const ModelInfo &info) const +{ + using namespace ModelSettingsKey; + return isUpgradeableModelSettingSet(info, PromptTemplate, ChatTemplate); +} + +auto MySettings::modelSystemMessage(const ModelInfo &info) const -> UpgradeableSetting +{ + using namespace ModelSettingsKey; + return getUpgradeableModelSetting(info, SystemPrompt, SystemMessage); +} + +bool MySettings::isModelSystemMessageSet(const ModelInfo &info) const +{ + using namespace ModelSettingsKey; + return isUpgradeableModelSettingSet(info, SystemPrompt, SystemMessage); +} + void MySettings::setModelFilename(const ModelInfo &info, const QString &value, bool force) { setModelSetting("filename", info, value, force, true); @@ -402,14 +506,77 @@ void MySettings::setModelRepeatPenaltyTokens(const ModelInfo &info, int value, b setModelSetting("repeatPenaltyTokens", info, value, force, true); } -void MySettings::setModelPromptTemplate(const ModelInfo &info, const QString &value, bool force) +bool MySettings::setUpgradeableModelSetting( + const ModelInfo &info, const QString &value, QLatin1StringView legacyKey, QLatin1StringView newKey +) { + if (info.id().isEmpty()) { + qWarning("%s: got null model", Q_FUNC_INFO); + return false; + } + + auto legacyModelKey = modelSettingName(info, legacyKey); + auto newModelKey = modelSettingName(info, newKey ); + bool changed = false; + if (m_settings.contains(legacyModelKey)) { + m_settings.remove(legacyModelKey); + changed = true; + } + auto oldValue = m_settings.value(newModelKey); + if (!oldValue.isValid() || oldValue.toString() != value) { + m_settings.setValue(newModelKey, value); + changed = true; + } + return changed; +} + +bool MySettings::resetUpgradeableModelSetting( + const ModelInfo &info, QLatin1StringView legacyKey, QLatin1StringView newKey +) { + if (info.id().isEmpty()) { + qWarning("%s: got null model", Q_FUNC_INFO); + return false; + } + + auto legacyModelKey = modelSettingName(info, legacyKey); + auto newModelKey = modelSettingName(info, newKey ); + bool changed = false; + if (m_settings.contains(legacyModelKey)) { + m_settings.remove(legacyModelKey); + changed = true; + } + if (m_settings.contains(newModelKey)) { + m_settings.remove(newModelKey); + changed = true; + } + return changed; +} + +void MySettings::setModelChatTemplate(const ModelInfo &info, const QString &value) +{ + using namespace ModelSettingsKey; + if (setUpgradeableModelSetting(info, value, PromptTemplate, ChatTemplate)) + emit chatTemplateChanged(info); +} + +void MySettings::resetModelChatTemplate(const ModelInfo &info) +{ + using namespace ModelSettingsKey; + if (resetUpgradeableModelSetting(info, PromptTemplate, ChatTemplate)) + emit chatTemplateChanged(info); +} + +void MySettings::setModelSystemMessage(const ModelInfo &info, const QString &value) { - setModelSetting("promptTemplate", info, value, force, true); + using namespace ModelSettingsKey; + if (setUpgradeableModelSetting(info, value, SystemPrompt, SystemMessage)) + emit systemMessageChanged(info); } -void MySettings::setModelSystemPrompt(const ModelInfo &info, const QString &value, bool force) +void MySettings::resetModelSystemMessage(const ModelInfo &info) { - setModelSetting("systemPrompt", info, value, force, true); + using namespace ModelSettingsKey; + if (resetUpgradeableModelSetting(info, SystemPrompt, SystemMessage)) + emit systemMessageChanged(info); } void MySettings::setModelChatNamePrompt(const ModelInfo &info, const QString &value, bool force) @@ -445,7 +612,6 @@ void MySettings::setThreadCount(int value) emit threadCountChanged(); } -bool MySettings::saveChatsContext() const { return getBasicSetting("saveChatsContext" ).toBool(); } bool MySettings::systemTray() const { return getBasicSetting("systemTray" ).toBool(); } bool MySettings::serverChat() const { return getBasicSetting("serverChat" ).toBool(); } int MySettings::networkPort() const { return getBasicSetting("networkPort" ).toInt(); } @@ -464,7 +630,6 @@ ChatTheme MySettings::chatTheme() const { return ChatTheme (getEnu FontSize MySettings::fontSize() const { return FontSize (getEnumSetting("fontSize", fontSizeNames)); } SuggestionMode MySettings::suggestionMode() const { return SuggestionMode(getEnumSetting("suggestionMode", suggestionModeNames)); } -void MySettings::setSaveChatsContext(bool value) { setBasicSetting("saveChatsContext", value); } void MySettings::setSystemTray(bool value) { setBasicSetting("systemTray", value); } void MySettings::setServerChat(bool value) { setBasicSetting("serverChat", value); } void MySettings::setNetworkPort(int value) { setBasicSetting("networkPort", value); } diff --git a/gpt4all-chat/src/mysettings.h b/gpt4all-chat/src/mysettings.h index f3d5e5b058b5..a1a61e0618e0 100644 --- a/gpt4all-chat/src/mysettings.h +++ b/gpt4all-chat/src/mysettings.h @@ -4,6 +4,9 @@ #include "modellist.h" // IWYU pragma: keep #include +#include +#include +#include #include #include #include @@ -48,7 +51,6 @@ class MySettings : public QObject { Q_OBJECT Q_PROPERTY(int threadCount READ threadCount WRITE setThreadCount NOTIFY threadCountChanged) - Q_PROPERTY(bool saveChatsContext READ saveChatsContext WRITE setSaveChatsContext NOTIFY saveChatsContextChanged) Q_PROPERTY(bool systemTray READ systemTray WRITE setSystemTray NOTIFY systemTrayChanged) Q_PROPERTY(bool serverChat READ serverChat WRITE setServerChat NOTIFY serverChatChanged) Q_PROPERTY(QString modelPath READ modelPath WRITE setModelPath NOTIFY modelPathChanged) @@ -75,9 +77,18 @@ class MySettings : public QObject Q_PROPERTY(SuggestionMode suggestionMode READ suggestionMode WRITE setSuggestionMode NOTIFY suggestionModeChanged) Q_PROPERTY(QStringList uiLanguages MEMBER m_uiLanguages CONSTANT) +private: + explicit MySettings(); + ~MySettings() override = default; + +public Q_SLOTS: + void onModelInfoChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles = {}); + public: static MySettings *globalInstance(); + Q_INVOKABLE static QVariant checkJinjaTemplateError(const QString &tmpl); + // Restore methods Q_INVOKABLE void restoreModelDefaults(const ModelInfo &info); Q_INVOKABLE void restoreApplicationDefaults(); @@ -125,10 +136,14 @@ class MySettings : public QObject Q_INVOKABLE void setModelRepeatPenalty(const ModelInfo &info, double value, bool force = false); int modelRepeatPenaltyTokens(const ModelInfo &info) const; Q_INVOKABLE void setModelRepeatPenaltyTokens(const ModelInfo &info, int value, bool force = false); - QString modelPromptTemplate(const ModelInfo &info) const; - Q_INVOKABLE void setModelPromptTemplate(const ModelInfo &info, const QString &value, bool force = false); - QString modelSystemPrompt(const ModelInfo &info) const; - Q_INVOKABLE void setModelSystemPrompt(const ModelInfo &info, const QString &value, bool force = false); + auto modelChatTemplate(const ModelInfo &info) const -> UpgradeableSetting; + Q_INVOKABLE bool isModelChatTemplateSet(const ModelInfo &info) const; + Q_INVOKABLE void setModelChatTemplate(const ModelInfo &info, const QString &value); + Q_INVOKABLE void resetModelChatTemplate(const ModelInfo &info); + auto modelSystemMessage(const ModelInfo &info) const -> UpgradeableSetting; + Q_INVOKABLE bool isModelSystemMessageSet(const ModelInfo &info) const; + Q_INVOKABLE void setModelSystemMessage(const ModelInfo &info, const QString &value); + Q_INVOKABLE void resetModelSystemMessage(const ModelInfo &info); int modelContextLength(const ModelInfo &info) const; Q_INVOKABLE void setModelContextLength(const ModelInfo &info, int value, bool force = false); int modelGpuLayers(const ModelInfo &info) const; @@ -141,8 +156,6 @@ class MySettings : public QObject // Application settings int threadCount() const; void setThreadCount(int value); - bool saveChatsContext() const; - void setSaveChatsContext(bool value); bool systemTray() const; void setSystemTray(bool value); bool serverChat() const; @@ -215,12 +228,11 @@ class MySettings : public QObject void gpuLayersChanged(const ModelInfo &info); void repeatPenaltyChanged(const ModelInfo &info); void repeatPenaltyTokensChanged(const ModelInfo &info); - void promptTemplateChanged(const ModelInfo &info); - void systemPromptChanged(const ModelInfo &info); + void chatTemplateChanged(const ModelInfo &info, bool fromInfo = false); + void systemMessageChanged(const ModelInfo &info, bool fromInfo = false); void chatNamePromptChanged(const ModelInfo &info); void suggestedFollowUpPromptChanged(const ModelInfo &info); void threadCountChanged(); - void saveChatsContextChanged(); void systemTrayChanged(); void serverChatChanged(); void modelPathChanged(); @@ -245,6 +257,30 @@ class MySettings : public QObject void suggestionModeChanged(); void languageAndLocaleChanged(); +private: + QVariant getBasicSetting(const QString &name) const; + void setBasicSetting(const QString &name, const QVariant &value, std::optional signal = std::nullopt); + int getEnumSetting(const QString &setting, const QStringList &valueNames) const; + QVariant getModelSetting(QLatin1StringView name, const ModelInfo &info) const; + QVariant getModelSetting(const char *name, const ModelInfo &info) const; + void setModelSetting(QLatin1StringView name, const ModelInfo &info, const QVariant &value, bool force, + bool signal = false); + void setModelSetting(const char *name, const ModelInfo &info, const QVariant &value, bool force, + bool signal = false); + auto getUpgradeableModelSetting( + const ModelInfo &info, QLatin1StringView legacyKey, QLatin1StringView newKey + ) const -> UpgradeableSetting; + bool isUpgradeableModelSettingSet( + const ModelInfo &info, QLatin1StringView legacyKey, QLatin1StringView newKey + ) const; + bool setUpgradeableModelSetting( + const ModelInfo &info, const QString &value, QLatin1StringView legacyKey, QLatin1StringView newKey + ); + bool resetUpgradeableModelSetting( + const ModelInfo &info, QLatin1StringView legacyKey, QLatin1StringView newKey + ); + QString filePathForLocale(const QLocale &locale); + private: QSettings m_settings; bool m_forceMetal; @@ -253,18 +289,7 @@ class MySettings : public QObject const QStringList m_uiLanguages; std::unique_ptr m_translator; -private: - explicit MySettings(); - ~MySettings() {} friend class MyPrivateSettings; - - QVariant getBasicSetting(const QString &name) const; - void setBasicSetting(const QString &name, const QVariant &value, std::optional signal = std::nullopt); - int getEnumSetting(const QString &setting, const QStringList &valueNames) const; - QVariant getModelSetting(const QString &name, const ModelInfo &info) const; - void setModelSetting(const QString &name, const ModelInfo &info, const QVariant &value, bool force, - bool signal = false); - QString filePathForLocale(const QLocale &locale); }; #endif // MYSETTINGS_H diff --git a/gpt4all-chat/src/network.cpp b/gpt4all-chat/src/network.cpp index 2e794d1ab241..bff380102ba2 100644 --- a/gpt4all-chat/src/network.cpp +++ b/gpt4all-chat/src/network.cpp @@ -8,6 +8,7 @@ #include "localdocsmodel.h" #include "modellist.h" #include "mysettings.h" +#include "utils.h" #include @@ -192,11 +193,14 @@ bool Network::packageAndSendJson(const QString &ingestId, const QString &json) return false; } + auto *currentChat = ChatListModel::globalInstance()->currentChat(); + Q_ASSERT(currentChat); + auto modelInfo = currentChat->modelInfo(); + Q_ASSERT(doc.isObject()); - Q_ASSERT(ChatListModel::globalInstance()->currentChat()); QJsonObject object = doc.object(); object.insert("source", "gpt4all-chat"); - object.insert("agent_id", ChatListModel::globalInstance()->currentChat()->modelInfo().filename()); + object.insert("agent_id", modelInfo.filename()); object.insert("submitter_id", m_uniqueId); object.insert("ingest_id", ingestId); @@ -204,8 +208,9 @@ bool Network::packageAndSendJson(const QString &ingestId, const QString &json) if (!attribution.isEmpty()) object.insert("network/attribution", attribution); - QString promptTemplate = ChatListModel::globalInstance()->currentChat()->modelInfo().promptTemplate(); - object.insert("prompt_template", promptTemplate); + if (!modelInfo.id().isNull()) + if (auto tmpl = modelInfo.chatTemplate().asModern()) + object.insert("chat_template"_L1, *tmpl); QJsonDocument newDoc; newDoc.setObject(object); @@ -358,7 +363,8 @@ void Network::sendStartup() void Network::trackChatEvent(const QString &ev, QVariantMap props) { - const auto &curChat = ChatListModel::globalInstance()->currentChat(); + auto *curChat = ChatListModel::globalInstance()->currentChat(); + Q_ASSERT(curChat); if (!props.contains("model")) props.insert("model", curChat->modelInfo().filename()); props.insert("device_backend", curChat->deviceBackend()); @@ -366,7 +372,7 @@ void Network::trackChatEvent(const QString &ev, QVariantMap props) props.insert("doc_collections_enabled", curChat->collectionList().count()); props.insert("doc_collections_total", LocalDocs::globalInstance()->localDocsModel()->rowCount()); props.insert("datalake_active", MySettings::globalInstance()->networkIsActive()); - props.insert("using_server", ChatListModel::globalInstance()->currentChat()->isServer()); + props.insert("using_server", curChat->isServer()); trackEvent(ev, props); } diff --git a/gpt4all-chat/src/server.cpp b/gpt4all-chat/src/server.cpp index 577859a3266b..20a3fa7a4d40 100644 --- a/gpt4all-chat/src/server.cpp +++ b/gpt4all-chat/src/server.cpp @@ -1,6 +1,7 @@ #include "server.h" #include "chat.h" +#include "chatmodel.h" #include "modellist.h" #include "mysettings.h" #include "utils.h" @@ -313,11 +314,8 @@ const std::unordered_map BaseCompleti class ChatRequest : public BaseCompletionRequest { public: struct Message { - enum class Role : uint8_t { - User, - Assistant, - }; - Role role; + enum class Role { System, User, Assistant }; + Role role; QString content; }; @@ -349,7 +347,6 @@ class ChatRequest : public BaseCompletionRequest { this->messages.clear(); { QCborArray arr = value.toArray(); - Message::Role nextRole = Message::Role::User; for (qsizetype i = 0; i < arr.size(); i++) { const auto &elem = arr[i]; if (!elem.isMap()) @@ -360,9 +357,9 @@ class ChatRequest : public BaseCompletionRequest { QCborMap msg = elem.toMap(); Message res; QString role = takeValue(msg, "role", String, /*required*/ true).toString(); - if (role == u"system"_s) - continue; // FIXME(jared): don't ignore these - if (role == u"user"_s) { + if (role == u"system"_s) { + res.role = Message::Role::System; + } else if (role == u"user"_s) { res.role = Message::Role::User; } else if (role == u"assistant"_s) { res.role = Message::Role::Assistant; @@ -374,13 +371,7 @@ class ChatRequest : public BaseCompletionRequest { )); } res.content = takeValue(msg, "content", String, /*required*/ true).toString(); - if (res.role != nextRole) - throw InvalidRequestError(fmt::format( - "Invalid 'messages[{}].role': did not expect '{}' here", i, role - )); this->messages.append(res); - nextRole = res.role == Message::Role::User ? Message::Role::Assistant - : Message::Role::User; if (!msg.isEmpty()) throw InvalidRequestError(fmt::format( @@ -630,8 +621,7 @@ void Server::start() }); #endif - connect(this, &Server::requestServerNewPromptResponsePair, m_chat, - &Chat::serverNewPromptResponsePair, Qt::BlockingQueuedConnection); + connect(this, &Server::requestResetResponseState, m_chat, &Chat::resetResponseState, Qt::BlockingQueuedConnection); } static auto makeError(auto &&...args) -> std::pair> @@ -642,6 +632,10 @@ static auto makeError(auto &&...args) -> std::pair std::pair> { + Q_ASSERT(m_chatModel); + + auto *mySettings = MySettings::globalInstance(); + ModelInfo modelInfo = ModelList::globalInstance()->defaultModelInfo(); const QList modelList = ModelList::globalInstance()->selectableModelList(); for (const ModelInfo &info : modelList) { @@ -654,10 +648,6 @@ auto Server::handleCompletionRequest(const CompletionRequest &request) } } - // adds prompt/response items to GUI - emit requestServerNewPromptResponsePair(request.prompt); // blocks - resetResponse(); - // load the new model if necessary setShouldBeLoaded(true); @@ -666,47 +656,56 @@ auto Server::handleCompletionRequest(const CompletionRequest &request) return makeError(QHttpServerResponder::StatusCode::InternalServerError); } + emit requestResetResponseState(); // blocks + qsizetype prevMsgIndex = m_chatModel->count() - 1; + if (prevMsgIndex >= 0) + m_chatModel->updateCurrentResponse(prevMsgIndex, false); + // NB: this resets the context, regardless of whether this model is already loaded if (!loadModel(modelInfo)) { std::cerr << "ERROR: couldn't load model " << modelInfo.name().toStdString() << std::endl; return makeError(QHttpServerResponder::StatusCode::InternalServerError); } + // add prompt/response items to GUI + m_chatModel->appendPrompt(request.prompt); + m_chatModel->appendResponse(); + // FIXME(jared): taking parameters from the UI inhibits reproducibility of results - const int top_k = modelInfo.topK(); - const int n_batch = modelInfo.promptBatchSize(); - const auto repeat_penalty = float(modelInfo.repeatPenalty()); - const int repeat_last_n = modelInfo.repeatPenaltyTokens(); + LLModel::PromptContext promptCtx { + .n_predict = request.max_tokens, + .top_k = mySettings->modelTopK(modelInfo), + .top_p = request.top_p, + .min_p = request.min_p, + .temp = request.temperature, + .n_batch = mySettings->modelPromptBatchSize(modelInfo), + .repeat_penalty = float(mySettings->modelRepeatPenalty(modelInfo)), + .repeat_last_n = mySettings->modelRepeatPenaltyTokens(modelInfo), + }; + auto promptUtf8 = request.prompt.toUtf8(); int promptTokens = 0; int responseTokens = 0; - QList>> responses; + QStringList responses; for (int i = 0; i < request.n; ++i) { - if (!promptInternal( - m_collections, - request.prompt, - /*promptTemplate*/ u"%1"_s, - request.max_tokens, - top_k, - request.top_p, - request.min_p, - request.temperature, - n_batch, - repeat_penalty, - repeat_last_n)) { - - std::cerr << "ERROR: couldn't prompt model " << modelInfo.name().toStdString() << std::endl; + PromptResult result; + try { + result = promptInternal(std::string_view(promptUtf8.cbegin(), promptUtf8.cend()), + promptCtx, + /*usedLocalDocs*/ false); + } catch (const std::exception &e) { + m_chatModel->setResponseValue(e.what()); + m_chatModel->setError(); + emit responseStopped(0); return makeError(QHttpServerResponder::StatusCode::InternalServerError); } - QString resp = response(/*trim*/ false); + QString resp = QString::fromUtf8(result.response); if (request.echo) resp = request.prompt + resp; - responses.append({resp, m_databaseResults}); - if (!promptTokens) - promptTokens = m_promptTokens; - responseTokens += m_promptResponseTokens - m_promptTokens; - if (i < request.n - 1) - resetResponse(); + responses << resp; + if (i == 0) + promptTokens = result.promptTokens; + responseTokens += result.responseTokens; } QJsonObject responseObject { @@ -717,25 +716,13 @@ auto Server::handleCompletionRequest(const CompletionRequest &request) }; QJsonArray choices; - { - int index = 0; - for (const auto &r : responses) { - QString result = r.first; - QList infos = r.second; - QJsonObject choice { - { "text", result }, - { "index", index++ }, - { "logprobs", QJsonValue::Null }, - { "finish_reason", responseTokens == request.max_tokens ? "length" : "stop" }, - }; - if (MySettings::globalInstance()->localDocsShowReferences()) { - QJsonArray references; - for (const auto &ref : infos) - references.append(resultToJson(ref)); - choice.insert("references", references.isEmpty() ? QJsonValue::Null : QJsonValue(references)); - } - choices.append(choice); - } + for (qsizetype i = 0; auto &resp : std::as_const(responses)) { + choices << QJsonObject { + { "text", resp }, + { "index", i++ }, + { "logprobs", QJsonValue::Null }, + { "finish_reason", responseTokens == request.max_tokens ? "length" : "stop" }, + }; } responseObject.insert("choices", choices); @@ -751,6 +738,8 @@ auto Server::handleCompletionRequest(const CompletionRequest &request) auto Server::handleChatRequest(const ChatRequest &request) -> std::pair> { + auto *mySettings = MySettings::globalInstance(); + ModelInfo modelInfo = ModelList::globalInstance()->defaultModelInfo(); const QList modelList = ModelList::globalInstance()->selectableModelList(); for (const ModelInfo &info : modelList) { @@ -771,83 +760,59 @@ auto Server::handleChatRequest(const ChatRequest &request) return makeError(QHttpServerResponder::StatusCode::InternalServerError); } + emit requestResetResponseState(); // blocks + // NB: this resets the context, regardless of whether this model is already loaded if (!loadModel(modelInfo)) { std::cerr << "ERROR: couldn't load model " << modelInfo.name().toStdString() << std::endl; return makeError(QHttpServerResponder::StatusCode::InternalServerError); } - const QString promptTemplate = modelInfo.promptTemplate(); - const int top_k = modelInfo.topK(); - const int n_batch = modelInfo.promptBatchSize(); - const auto repeat_penalty = float(modelInfo.repeatPenalty()); - const int repeat_last_n = modelInfo.repeatPenaltyTokens(); + m_chatModel->updateCurrentResponse(m_chatModel->count() - 1, false); - int promptTokens = 0; - int responseTokens = 0; - QList>> responses; Q_ASSERT(!request.messages.isEmpty()); - Q_ASSERT(request.messages.size() % 2 == 1); - for (int i = 0; i < request.messages.size() - 2; i += 2) { + + // adds prompt/response items to GUI + std::vector messages; + for (auto &message : request.messages) { using enum ChatRequest::Message::Role; - auto &user = request.messages[i]; - auto &assistant = request.messages[i + 1]; - Q_ASSERT(user.role == User); - Q_ASSERT(assistant.role == Assistant); - - // adds prompt/response items to GUI - emit requestServerNewPromptResponsePair(user.content); // blocks - resetResponse(); - - if (!promptInternal( - {}, - user.content, - promptTemplate, - request.max_tokens, - top_k, - request.top_p, - request.min_p, - request.temperature, - n_batch, - repeat_penalty, - repeat_last_n, - assistant.content) - ) { - std::cerr << "ERROR: couldn't prompt model " << modelInfo.name().toStdString() << std::endl; - return makeError(QHttpServerResponder::StatusCode::InternalServerError); + switch (message.role) { + case System: messages.push_back({ MessageInput::Type::System, message.content }); break; + case User: messages.push_back({ MessageInput::Type::Prompt, message.content }); break; + case Assistant: messages.push_back({ MessageInput::Type::Response, message.content }); break; } - promptTokens += m_promptResponseTokens; // previous responses are part of current prompt } + auto startOffset = m_chatModel->appendResponseWithHistory(messages); - QString lastMessage = request.messages.last().content; - // adds prompt/response items to GUI - emit requestServerNewPromptResponsePair(lastMessage); // blocks - resetResponse(); + // FIXME(jared): taking parameters from the UI inhibits reproducibility of results + LLModel::PromptContext promptCtx { + .n_predict = request.max_tokens, + .top_k = mySettings->modelTopK(modelInfo), + .top_p = request.top_p, + .min_p = request.min_p, + .temp = request.temperature, + .n_batch = mySettings->modelPromptBatchSize(modelInfo), + .repeat_penalty = float(mySettings->modelRepeatPenalty(modelInfo)), + .repeat_last_n = mySettings->modelRepeatPenaltyTokens(modelInfo), + }; + int promptTokens = 0; + int responseTokens = 0; + QList>> responses; for (int i = 0; i < request.n; ++i) { - if (!promptInternal( - m_collections, - lastMessage, - promptTemplate, - request.max_tokens, - top_k, - request.top_p, - request.min_p, - request.temperature, - n_batch, - repeat_penalty, - repeat_last_n) - ) { - std::cerr << "ERROR: couldn't prompt model " << modelInfo.name().toStdString() << std::endl; + ChatPromptResult result; + try { + result = promptInternalChat(m_collections, promptCtx, startOffset); + } catch (const std::exception &e) { + m_chatModel->setResponseValue(e.what()); + m_chatModel->setError(); + emit responseStopped(0); return makeError(QHttpServerResponder::StatusCode::InternalServerError); } - responses.append({response(), m_databaseResults}); - // FIXME(jared): these are UI counts and do not include framing tokens, which they should + responses.emplace_back(result.response, result.databaseResults); if (i == 0) - promptTokens += m_promptTokens; - responseTokens += m_promptResponseTokens - m_promptTokens; - if (i != request.n - 1) - resetResponse(); + promptTokens = result.promptTokens; + responseTokens += result.responseTokens; } QJsonObject responseObject { diff --git a/gpt4all-chat/src/server.h b/gpt4all-chat/src/server.h index a5447d865921..092624e9ed61 100644 --- a/gpt4all-chat/src/server.h +++ b/gpt4all-chat/src/server.h @@ -2,7 +2,6 @@ #define SERVER_H #include "chatllm.h" -#include "chatmodel.h" #include "database.h" #include @@ -33,7 +32,7 @@ public Q_SLOTS: void start(); Q_SIGNALS: - void requestServerNewPromptResponsePair(const QString &prompt, const QList &attachments = {}); + void requestResetResponseState(); private: auto handleCompletionRequest(const CompletionRequest &request) -> std::pair>; diff --git a/gpt4all-chat/src/tool.cpp b/gpt4all-chat/src/tool.cpp new file mode 100644 index 000000000000..4aa877f26c2d --- /dev/null +++ b/gpt4all-chat/src/tool.cpp @@ -0,0 +1,73 @@ +#include "tool.h" + +#include + +using json = nlohmann::ordered_json; + + +json::object_t Tool::jinjaValue() const +{ + json::array_t paramList; + const QList p = parameters(); + for (auto &info : p) { + std::string typeStr; + switch (info.type) { + using enum ToolEnums::ParamType; + case String: typeStr = "string"; break; + case Number: typeStr = "number"; break; + case Integer: typeStr = "integer"; break; + case Object: typeStr = "object"; break; + case Array: typeStr = "array"; break; + case Boolean: typeStr = "boolean"; break; + case Null: typeStr = "null"; break; + } + paramList.emplace_back(json::initializer_list_t { + { "name", info.name.toStdString() }, + { "type", typeStr }, + { "description", info.description.toStdString() }, + { "required", info.required }, + }); + } + + return { + { "name", name().toStdString() }, + { "description", description().toStdString() }, + { "function", function().toStdString() }, + { "parameters", paramList }, + { "symbolicFormat", symbolicFormat().toStdString() }, + { "examplePrompt", examplePrompt().toStdString() }, + { "exampleCall", exampleCall().toStdString() }, + { "exampleReply", exampleReply().toStdString() }, + }; +} + +void ToolCallInfo::serialize(QDataStream &stream, int version) +{ + stream << name; + stream << params.size(); + for (auto param : params) { + stream << param.name; + stream << param.type; + stream << param.value; + } + stream << result; + stream << error; + stream << errorString; +} + +bool ToolCallInfo::deserialize(QDataStream &stream, int version) +{ + stream >> name; + qsizetype count; + stream >> count; + for (int i = 0; i < count; ++i) { + ToolParam p; + stream >> p.name; + stream >> p.type; + stream >> p.value; + } + stream >> result; + stream >> error; + stream >> errorString; + return true; +} diff --git a/gpt4all-chat/src/tool.h b/gpt4all-chat/src/tool.h new file mode 100644 index 000000000000..0af645f5fe9a --- /dev/null +++ b/gpt4all-chat/src/tool.h @@ -0,0 +1,135 @@ +#ifndef TOOL_H +#define TOOL_H + +#include + +#include +#include +#include +#include +#include + +using json = nlohmann::ordered_json; + + +namespace ToolEnums +{ + Q_NAMESPACE + enum class Error + { + NoError = 0, + TimeoutError = 2, + UnknownError = 499, + }; + Q_ENUM_NS(Error) + + enum class ParamType { String, Number, Integer, Object, Array, Boolean, Null }; // json schema types + Q_ENUM_NS(ParamType) + + enum class ParseState { + None, + InTagChoice, + InStart, + Partial, + Complete, + }; + Q_ENUM_NS(ParseState) +} + +struct ToolParamInfo +{ + QString name; + ToolEnums::ParamType type; + QString description; + bool required; +}; +Q_DECLARE_METATYPE(ToolParamInfo) + +struct ToolParam +{ + QString name; + ToolEnums::ParamType type; + QVariant value; + bool operator==(const ToolParam& other) const + { + return name == other.name && type == other.type && value == other.value; + } +}; +Q_DECLARE_METATYPE(ToolParam) + +struct ToolCallInfo +{ + QString name; + QList params; + QString result; + ToolEnums::Error error = ToolEnums::Error::NoError; + QString errorString; + + void serialize(QDataStream &stream, int version); + bool deserialize(QDataStream &stream, int version); + + bool operator==(const ToolCallInfo& other) const + { + return name == other.name && result == other.result && params == other.params + && error == other.error && errorString == other.errorString; + } +}; +Q_DECLARE_METATYPE(ToolCallInfo) + +class Tool : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString name READ name CONSTANT) + Q_PROPERTY(QString description READ description CONSTANT) + Q_PROPERTY(QString function READ function CONSTANT) + Q_PROPERTY(QList parameters READ parameters CONSTANT) + Q_PROPERTY(QString examplePrompt READ examplePrompt CONSTANT) + Q_PROPERTY(QString exampleCall READ exampleCall CONSTANT) + Q_PROPERTY(QString exampleReply READ exampleReply CONSTANT) + +public: + Tool() : QObject(nullptr) {} + virtual ~Tool() {} + + virtual void run(const QList ¶ms) = 0; + virtual bool interrupt() = 0; + + // Tools should set these if they encounter errors. For instance, a tool depending upon the network + // might set these error variables if the network is not available. + virtual ToolEnums::Error error() const { return ToolEnums::Error::NoError; } + virtual QString errorString() const { return QString(); } + + // [Required] Human readable name of the tool. + virtual QString name() const = 0; + + // [Required] Human readable description of what the tool does. Use this tool to: {{description}} + virtual QString description() const = 0; + + // [Required] Must be unique. Name of the function to invoke. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64. + virtual QString function() const = 0; + + // [Optional] List describing the tool's parameters. An empty list specifies no parameters. + virtual QList parameters() const { return {}; } + + // [Optional] The symbolic format of the toolcall. + virtual QString symbolicFormat() const { return QString(); } + + // [Optional] A human generated example of a prompt that could result in this tool being called. + virtual QString examplePrompt() const { return QString(); } + + // [Optional] An example of this tool call that pairs with the example query. It should be the + // complete string that the model must generate. + virtual QString exampleCall() const { return QString(); } + + // [Optional] An example of the reply the model might generate given the result of the tool call. + virtual QString exampleReply() const { return QString(); } + + bool operator==(const Tool &other) const { return function() == other.function(); } + + json::object_t jinjaValue() const; + +Q_SIGNALS: + void runComplete(const ToolCallInfo &info); +}; + +#endif // TOOL_H diff --git a/gpt4all-chat/src/toolcallparser.cpp b/gpt4all-chat/src/toolcallparser.cpp new file mode 100644 index 000000000000..bd45668791ac --- /dev/null +++ b/gpt4all-chat/src/toolcallparser.cpp @@ -0,0 +1,188 @@ +#include "toolcallparser.h" + +#include +#include +#include + +#include + +ToolCallParser::ToolCallParser() +{ + m_possibleStartTags << ToolCallConstants::CodeInterpreterTag.toUtf8() + << ToolCallConstants::ThinkTag.toUtf8(); + m_possibleEndTags << ToolCallConstants::CodeInterpreterEndTag.toUtf8() + << ToolCallConstants::ThinkEndTag.toUtf8(); + reset(); +} + +void ToolCallParser::reset() +{ + // Resets the search state, but not the buffer or global state + resetSearchState(); + + // These are global states maintained between update calls + m_buffers.clear(); + m_buffers << QByteArray(); +} + +void ToolCallParser::resetSearchState() +{ + m_expected = {'<'}; + m_expectedIndex = 0; + m_state = ToolEnums::ParseState::None; + + m_toolCall.clear(); + m_startTagBuffer.clear(); + m_endTagBuffer.clear(); + + m_currentTagIndex = -1; + m_startIndex = -1; + m_endIndex = -1; +} + +bool ToolCallParser::isExpected(char c) const +{ + return m_expected.isEmpty() || m_expected.contains(c); +} + +void ToolCallParser::setExpected(const QList &tags) +{ + m_expected.clear(); + for (const auto &tag : tags) { + Q_ASSERT(tag.size() > m_expectedIndex); + m_expected << tag.at(m_expectedIndex); + } +} + +QByteArray ToolCallParser::startTag() const +{ + if (m_currentTagIndex < 0) + return {}; + return m_possibleStartTags.at(m_currentTagIndex); +} + +QByteArray ToolCallParser::endTag() const +{ + if (m_currentTagIndex < 0) + return {}; + return m_possibleEndTags.at(m_currentTagIndex); +} + +QByteArray &ToolCallParser::currentBuffer() +{ + return m_buffers.last(); +} + +// This method is called with an arbitrary string and a current state. This method should take the +// current state into account and then parse through the update character by character to arrive at +// the new state. +void ToolCallParser::update(const QByteArray &update) +{ + currentBuffer().append(update); + + for (size_t i = currentBuffer().size() - update.size(); i < currentBuffer().size(); ++i) { + const char c = currentBuffer()[i]; + const bool foundMatch = isExpected(c); + if (!foundMatch) { + resetSearchState(); + continue; + } + + switch (m_state) { + case ToolEnums::ParseState::None: + { + m_expectedIndex = 1; + setExpected(m_possibleStartTags); + m_state = ToolEnums::ParseState::InTagChoice; + m_startIndex = i; + break; + } + case ToolEnums::ParseState::InTagChoice: + { + for (int i = 0; i < m_possibleStartTags.size(); ++i) { + const auto &tag = m_possibleStartTags.at(i); + if (c == tag.at(1)) m_currentTagIndex = i; + } + if (m_currentTagIndex >= 0) { + m_expectedIndex = 2; + setExpected({m_possibleStartTags.at(m_currentTagIndex)}); + m_state = ToolEnums::ParseState::InStart; + } else + resetSearchState(); + break; + } + case ToolEnums::ParseState::InStart: + { + m_startTagBuffer.append(c); + + const auto startTag = this->startTag(); + Q_ASSERT(!startTag.isEmpty()); + if (m_expectedIndex == startTag.size() - 1) { + m_expectedIndex = 0; + setExpected({}); + m_state = ToolEnums::ParseState::Partial; + } else { + ++m_expectedIndex; + Q_ASSERT(m_currentTagIndex >= 0); + setExpected({startTag}); + } + break; + } + case ToolEnums::ParseState::Partial: + { + Q_ASSERT(m_currentTagIndex >= 0); + const auto endTag = this->endTag(); + Q_ASSERT(!endTag.isEmpty()); + m_toolCall.append(c); + m_endTagBuffer.append(c); + if (m_endTagBuffer.size() > endTag.size()) + m_endTagBuffer.remove(0, 1); + if (m_endTagBuffer == endTag) { + m_endIndex = i + 1; + m_toolCall.chop(endTag.size()); + m_state = ToolEnums::ParseState::Complete; + m_endTagBuffer.clear(); + } + break; + } + case ToolEnums::ParseState::Complete: + { + // Already complete, do nothing further + break; + } + } + } +} + +bool ToolCallParser::splitIfPossible() +{ + // The first split happens when we're in a partial state + if (m_buffers.size() < 2 && m_state == ToolEnums::ParseState::Partial) { + Q_ASSERT(m_startIndex >= 0); + const auto beforeToolCall = currentBuffer().left(m_startIndex); + const auto toolCall = currentBuffer().mid (m_startIndex); + m_buffers = { beforeToolCall, toolCall }; + return true; + } + + // The second split happens when we're in the complete state + if (m_buffers.size() < 3 && m_state == ToolEnums::ParseState::Complete) { + Q_ASSERT(m_endIndex >= 0); + const auto &beforeToolCall = m_buffers.first(); + const auto toolCall = currentBuffer().left(m_endIndex); + const auto afterToolCall = currentBuffer().mid (m_endIndex); + m_buffers = { beforeToolCall, toolCall, afterToolCall }; + return true; + } + + return false; +} + +QStringList ToolCallParser::buffers() const +{ + QStringList result; + result.reserve(m_buffers.size()); + for (const auto &buffer : m_buffers) + result << QString::fromUtf8(buffer); + return result; +} diff --git a/gpt4all-chat/src/toolcallparser.h b/gpt4all-chat/src/toolcallparser.h new file mode 100644 index 000000000000..fb5140ccc301 --- /dev/null +++ b/gpt4all-chat/src/toolcallparser.h @@ -0,0 +1,61 @@ +#ifndef TOOLCALLPARSER_H +#define TOOLCALLPARSER_H + +#include "tool.h" + +#include +#include +#include +#include + +namespace ToolCallConstants +{ + const QString CodeInterpreterFunction = R"(javascript_interpret)"; + const QString CodeInterpreterTag = R"(<)" + CodeInterpreterFunction + R"(>)"; + const QString CodeInterpreterEndTag = R"()"; + const QString CodeInterpreterPrefix = CodeInterpreterTag + "\n```javascript\n"; + const QString CodeInterpreterSuffix = "```\n" + CodeInterpreterEndTag; + + // NB: the parsing code assumes the first char of the various tags differ + const QString ThinkTag = QStringLiteral(""); + const QString ThinkEndTag = QStringLiteral(""); +} + +class ToolCallParser +{ +public: + ToolCallParser(); + void reset(); + void update(const QByteArray &update); + QString toolCall() const { return QString::fromUtf8(m_toolCall); } + int startIndex() const { return m_startIndex; } + ToolEnums::ParseState state() const { return m_state; } + QByteArray startTag() const; + QByteArray endTag() const; + + bool splitIfPossible(); + QStringList buffers() const; + int numberOfBuffers() const { return m_buffers.size(); } + +private: + QByteArray ¤tBuffer(); + void resetSearchState(); + bool isExpected(char c) const; + void setExpected(const QList &tags); + + QList m_possibleStartTags; + QList m_possibleEndTags; + QByteArray m_startTagBuffer; + QByteArray m_endTagBuffer; + int m_currentTagIndex; + + QList m_expected; + int m_expectedIndex; + ToolEnums::ParseState m_state; + QList m_buffers; + QByteArray m_toolCall; + int m_startIndex; + int m_endIndex; +}; + +#endif // TOOLCALLPARSER_H diff --git a/gpt4all-chat/src/toolmodel.cpp b/gpt4all-chat/src/toolmodel.cpp new file mode 100644 index 000000000000..aade79f82dca --- /dev/null +++ b/gpt4all-chat/src/toolmodel.cpp @@ -0,0 +1,31 @@ +#include "toolmodel.h" + +#include "codeinterpreter.h" + +#include +#include +#include + +class MyToolModel: public ToolModel { }; +Q_GLOBAL_STATIC(MyToolModel, toolModelInstance) +ToolModel *ToolModel::globalInstance() +{ + return toolModelInstance(); +} + +ToolModel::ToolModel() + : QAbstractListModel(nullptr) +{ + QCoreApplication::instance()->installEventFilter(this); + + Tool* codeInterpreter = new CodeInterpreter; + m_tools.append(codeInterpreter); + m_toolMap.insert(codeInterpreter->function(), codeInterpreter); +} + +bool ToolModel::eventFilter(QObject *obj, QEvent *ev) +{ + if (obj == QCoreApplication::instance() && ev->type() == QEvent::LanguageChange) + emit dataChanged(index(0, 0), index(m_tools.size() - 1, 0)); + return false; +} diff --git a/gpt4all-chat/src/toolmodel.h b/gpt4all-chat/src/toolmodel.h new file mode 100644 index 000000000000..b20e39ccffdf --- /dev/null +++ b/gpt4all-chat/src/toolmodel.h @@ -0,0 +1,110 @@ +#ifndef TOOLMODEL_H +#define TOOLMODEL_H + +#include "tool.h" + +#include +#include +#include +#include +#include +#include +#include + +class ToolModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + static ToolModel *globalInstance(); + + enum Roles { + NameRole = Qt::UserRole + 1, + DescriptionRole, + FunctionRole, + ParametersRole, + SymbolicFormatRole, + ExamplePromptRole, + ExampleCallRole, + ExampleReplyRole, + }; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + Q_UNUSED(parent) + return m_tools.size(); + } + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override + { + if (!index.isValid() || index.row() < 0 || index.row() >= m_tools.size()) + return QVariant(); + + const Tool *item = m_tools.at(index.row()); + switch (role) { + case NameRole: + return item->name(); + case DescriptionRole: + return item->description(); + case FunctionRole: + return item->function(); + case ParametersRole: + return QVariant::fromValue(item->parameters()); + case SymbolicFormatRole: + return item->symbolicFormat(); + case ExamplePromptRole: + return item->examplePrompt(); + case ExampleCallRole: + return item->exampleCall(); + case ExampleReplyRole: + return item->exampleReply(); + } + + return QVariant(); + } + + QHash roleNames() const override + { + QHash roles; + roles[NameRole] = "name"; + roles[DescriptionRole] = "description"; + roles[FunctionRole] = "function"; + roles[ParametersRole] = "parameters"; + roles[SymbolicFormatRole] = "symbolicFormat"; + roles[ExamplePromptRole] = "examplePrompt"; + roles[ExampleCallRole] = "exampleCall"; + roles[ExampleReplyRole] = "exampleReply"; + return roles; + } + + Q_INVOKABLE Tool* get(int index) const + { + if (index < 0 || index >= m_tools.size()) return nullptr; + return m_tools.at(index); + } + + Q_INVOKABLE Tool *get(const QString &id) const + { + if (!m_toolMap.contains(id)) return nullptr; + return m_toolMap.value(id); + } + + int count() const { return m_tools.size(); } + +Q_SIGNALS: + void countChanged(); + void valueChanged(int index, const QString &value); + +protected: + bool eventFilter(QObject *obj, QEvent *ev) override; + +private: + explicit ToolModel(); + ~ToolModel() {} + friend class MyToolModel; + QList m_tools; + QHash m_toolMap; +}; + +#endif // TOOLMODEL_H diff --git a/gpt4all-chat/src/utils.h b/gpt4all-chat/src/utils.h index 0eacfe8bceb6..ac67e892cb40 100644 --- a/gpt4all-chat/src/utils.h +++ b/gpt4all-chat/src/utils.h @@ -3,23 +3,41 @@ #include #include +#include +#include +#include #include +#include +#include #include -#include +#include +#include +#include + +class QJsonObject; // fmtlib formatters for QString and QVariant -#define MAKE_FORMATTER(type, conversion) \ - template <> \ - struct fmt::formatter: fmt::formatter { \ - template \ - FmtContext::iterator format(const type &value, FmtContext &ctx) const \ - { \ - return formatter::format(conversion, ctx); \ - } \ +#define MAKE_FORMATTER(type, conversion) \ + template <> \ + struct fmt::formatter: fmt::formatter { \ + template \ + FmtContext::iterator format(const type &value, FmtContext &ctx) const \ + { \ + auto valueUtf8 = (conversion); \ + std::string_view view(valueUtf8.cbegin(), valueUtf8.cend()); \ + return formatter::format(view, ctx); \ + } \ } -MAKE_FORMATTER(QString, value.toStdString() ); -MAKE_FORMATTER(QVariant, value.toString().toStdString()); +MAKE_FORMATTER(QUtf8StringView, value ); +MAKE_FORMATTER(QStringView, value.toUtf8() ); +MAKE_FORMATTER(QString, value.toUtf8() ); +MAKE_FORMATTER(QVariant, value.toString().toUtf8()); + +// alternative to QJsonObject's initializer_list constructor that accepts Latin-1 strings +QJsonObject makeJsonObject(std::initializer_list> args); + +#include "utils.inl" diff --git a/gpt4all-chat/src/utils.inl b/gpt4all-chat/src/utils.inl new file mode 100644 index 000000000000..8aeb1f88c504 --- /dev/null +++ b/gpt4all-chat/src/utils.inl @@ -0,0 +1,9 @@ +#include + +inline QJsonObject makeJsonObject(std::initializer_list> args) +{ + QJsonObject obj; + for (auto &arg : args) + obj.insert(arg.first, arg.second); + return obj; +} diff --git a/gpt4all-chat/tests/python/test_server_api.py b/gpt4all-chat/tests/python/test_server_api.py index e1b0e476f4e9..449d8e2533c1 100644 --- a/gpt4all-chat/tests/python/test_server_api.py +++ b/gpt4all-chat/tests/python/test_server_api.py @@ -203,11 +203,10 @@ def test_with_models_empty(chat_server: None) -> None: EXPECTED_COMPLETIONS_RESPONSE = { 'choices': [ { - 'finish_reason': 'stop', + 'finish_reason': 'length', 'index': 0, 'logprobs': None, - 'references': None, - 'text': ' jumps over the lazy dog.', + 'text': ' jumps over the lazy dog.\n', }, ], 'id': 'placeholder', @@ -242,23 +241,19 @@ def test_with_models(chat_server_with_model: None) -> None: 'type': 'invalid_request_error', }} - data = { - 'model': 'Llama 3.2 1B Instruct', - 'prompt': 'The quick brown fox', - 'temperature': 0, - } - + data = dict( + model = 'Llama 3.2 1B Instruct', + prompt = 'The quick brown fox', + temperature = 0, + max_tokens = 6, + ) response = request.post('completions', data=data) - assert len(response['choices']) == 1 - assert response['choices'][0].keys() == {'text', 'index', 'logprobs', 'references', 'finish_reason'} - assert response['choices'][0]['text'] == ' jumps over the lazy dog.' - assert 'created' in response - response.pop('created') # Remove the dynamic field for comparison + del response['created'] # Remove the dynamic field for comparison assert response == EXPECTED_COMPLETIONS_RESPONSE -@pytest.mark.xfail(reason='Assertion failure in GPT4All. See nomic-ai/gpt4all#3133') def test_with_models_temperature(chat_server_with_model: None) -> None: + """Fixed by nomic-ai/gpt4all#3202.""" data = { 'model': 'Llama 3.2 1B Instruct', 'prompt': 'The quick brown fox', diff --git a/gpt4all-chat/translations/gpt4all_en_US.ts b/gpt4all-chat/translations/gpt4all_en_US.ts index ea4b79b8bd32..33ce242a5671 100644 --- a/gpt4all-chat/translations/gpt4all_en_US.ts +++ b/gpt4all-chat/translations/gpt4all_en_US.ts @@ -60,291 +60,486 @@ - AddModelView + AddGPT4AllModelView - - ← Existing Models + + These models have been specifically configured for use in GPT4All. The first few models on the list are known to work the best, but you should only attempt to use models that will fit in your available memory. - - Explore Models + + Network error: could not retrieve %1 - - Discover and download models by keyword search... + + + Busy indicator - - Text field for discovering and filtering downloadable models + + Displayed when the models request is ongoing - - Initiate model discovery and filtering + + Model file - - Triggers discovery and filtering of models + + Model file to be downloaded - - Default + + Description - - Likes + + File description - - Downloads + + Cancel - - Recent + + Resume - - Asc + + Download - - Desc + + Stop/restart/start the download - - None + + Remove + + + + + Remove model from filesystem + + + + + + Install + + + + + Install online model + + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + + + + + Describes an error that occurred when downloading + + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + + + + + Error for incompatible hardware + + + + + Download progressBar + + + + + Shows the progress made in the download + + + + + Download speed + + + + + Download speed in bytes/kilobytes/megabytes per second + + + + + Calculating... + + + + + + + + Whether the file hash is being calculated + + + + + Displayed when the file hash is being calculated + + + + + ERROR: $API_KEY is empty. + + + + + enter $API_KEY - + + ERROR: $BASE_URL is empty. + + + + + enter $BASE_URL + + + + + ERROR: $MODEL_NAME is empty. + + + + + enter $MODEL_NAME + + + + + File size + + + + + RAM required + + + + + %1 GB + + + + + + ? + + + + + Parameters + + + + + Quant + + + + + Type + + + + + AddHFModelView + + + Use the search to find and download models from HuggingFace. There is NO GUARANTEE that these will work. Many will require additional configuration before they can be used. + + + + + Discover and download models by keyword search... + + + + + Text field for discovering and filtering downloadable models + + + + Searching · %1 - + + Initiate model discovery and filtering + + + + + Triggers discovery and filtering of models + + + + + Default + + + + + Likes + + + + + Downloads + + + + + Recent + + + + Sort by: %1 - - Sort dir: %1 + + Asc - - Limit: %1 + + Desc - - Network error: could not retrieve %1 + + Sort dir: %1 - - - Busy indicator + + None - - Displayed when the models request is ongoing + + Limit: %1 - + Model file - + Model file to be downloaded - + Description - + File description - + Cancel - + Resume - + Download - + Stop/restart/start the download - + Remove - + Remove model from filesystem - - + + Install - + Install online model - - <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + + <strong><font size="1"><a href="#error">Error</a></strong></font> - - ERROR: $API_KEY is empty. + + Describes an error that occurred when downloading - - ERROR: $BASE_URL is empty. + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> - - enter $BASE_URL + + Error for incompatible hardware - - ERROR: $MODEL_NAME is empty. + + Download progressBar - - enter $MODEL_NAME + + Shows the progress made in the download - - %1 GB + + Download speed - - - ? + + Download speed in bytes/kilobytes/megabytes per second - - Describes an error that occurred when downloading + + Calculating... - - <strong><font size="1"><a href="#error">Error</a></strong></font> + + + + + Whether the file hash is being calculated - - Error for incompatible hardware + + Busy indicator - - Download progressBar + + Displayed when the file hash is being calculated - - Shows the progress made in the download + + ERROR: $API_KEY is empty. - - Download speed + + enter $API_KEY - - Download speed in bytes/kilobytes/megabytes per second + + ERROR: $BASE_URL is empty. - - Calculating... + + enter $BASE_URL - - - - - Whether the file hash is being calculated + + ERROR: $MODEL_NAME is empty. - - Displayed when the file hash is being calculated + + enter $MODEL_NAME - - enter $API_KEY + + File size - - File size + + Quant - - RAM required + + Type + + + AddModelView - - Parameters + + ← Existing Models - - Quant + + Explore Models - - Type + + GPT4All + + + + + HuggingFace @@ -548,12 +743,12 @@ - Save Chat Context + Enable System Tray - Save the chat model's state to disk for faster loading. WARNING: Uses ~2GB per chat. + The application will minimize to the system tray when the window is closed. @@ -595,13 +790,13 @@ Chat - - + + New Chat - + Server Chat @@ -609,12 +804,12 @@ ChatAPIWorker - + ERROR: Network error occurred while connecting to the API server - + ChatAPIWorker::handleFinished got HTTP Error %1 %2 @@ -682,35 +877,181 @@ + + ChatItemView + + + GPT4All + + + + + You + + + + + response stopped ... + + + + + retrieving localdocs: %1 ... + + + + + searching localdocs: %1 ... + + + + + processing ... + + + + + generating response ... + + + + + generating questions ... + + + + + + Copy + + + + + Copy Message + + + + + Disable markdown + + + + + Enable markdown + + + + + %n Source(s) + + %n Source + %n Sources + + + + + LocalDocs + + + + + Edit this message? + + + + + + All following messages will be permanently erased. + + + + + Redo this response? + + + + + Cannot edit chat without a loaded model. + + + + + Cannot edit chat while the model is generating. + + + + + Edit + + + + + Cannot redo response without a loaded model. + + + + + Cannot redo response while the model is generating. + + + + + Redo + + + + + Like response + + + + + Dislike response + + + + + Suggested follow-ups + + + + + ChatLLM + + + Your message was too long and could not be processed (%1 > %2). Please try again with something shorter. + + + ChatListModel - + TODAY - + THIS WEEK - + THIS MONTH - + LAST SIX MONTHS - + THIS YEAR - + LAST YEAR @@ -718,349 +1059,282 @@ ChatView - + <h3>Warning</h3><p>%1</p> - - Switch model dialog - - - - - Warn the user if they switch models, then context will be erased + + Conversation copied to clipboard. - - Conversation copied to clipboard. + + Code copied to clipboard. - - Code copied to clipboard. + + The entire chat will be erased. - + Chat panel - + Chat panel with options - + Reload the currently loaded model - + Eject the currently loaded model - + No model installed. - + Model loading error. - + Waiting for model... - + Switching context... - + Choose a model... - + Not found: %1 - + The top item is the current model - - + LocalDocs - + Add documents - + add collections of documents to the chat - + Load the default model - + Loads the default model which can be changed in settings - + No Model Installed - + GPT4All requires that you install at least one model to get started - + Install a Model - + Shows the add model view - + Conversation with the model - + prompt / response pairs from the conversation - - GPT4All - - - - - You - - - - - response stopped ... + + Legacy prompt template needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. - - processing ... + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. - - generating response ... + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. - - generating questions ... + + Legacy system prompt needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. - - + Copy - - - Copy Message - - - - - Disable markdown - - - - - Enable markdown - - - - - Thumbs up - - - - - Gives a thumbs up to the response - - - - - Thumbs down - - - - - Opens thumbs down dialog - - - %n Source(s) - + %n Source %n Sources - - Suggested follow-ups - - - - + Erase and reset chat session - + Copy chat session to clipboard - - Redo last chat response - - - - + Add media - + Adds media to the prompt - + Stop generating - + Stop the current response generation - + Attach - + Single File - + Reloads the model - + <h3>Encountered an error loading model:</h3><br><i>"%1"</i><br><br>Model loading failures can happen for a variety of reasons, but the most common causes include a bad file format, an incomplete or corrupted download, the wrong file type, not enough system RAM or an incompatible model type. Here are some suggestions for resolving the problem:<br><ul><li>Ensure the model file has a compatible format and type<li>Check the model file is complete in the download folder<li>You can find the download folder in the settings dialog<li>If you've sideloaded the model ensure the file is not corrupt by checking md5sum<li>Read more about what models are supported in our <a href="https://docs.gpt4all.io/">documentation</a> for the gui<li>Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help - - - Reload · %1 - - - - - Loading · %1 + + + Erase conversation? - - Load · %1 (default) → + + Changing the model will erase the current conversation. - - restoring from text ... + + + Reload · %1 - - retrieving localdocs: %1 ... + + Loading · %1 - - searching localdocs: %1 ... + + Load · %1 (default) → - + Send a message... - + Load a model to continue... - + Send messages/prompts to the model - + Cut - + Paste - + Select All - + Send message - + Sends the message/prompt contained in textfield to the model @@ -1104,40 +1378,53 @@ model to get started + + ConfirmationDialog + + + OK + + + + + Cancel + + + Download - + Model "%1" is installed successfully. - + ERROR: $MODEL_NAME is empty. - + ERROR: $API_KEY is empty. - + ERROR: $BASE_URL is invalid. - + ERROR: Model "%1 (%2)" is conflict. - + Model "%1 (%2)" is installed successfully. - + Model "%1" is removed. @@ -1303,52 +1590,52 @@ model to get started - + Display - + Show Sources - + Display the sources used for each response. - + Advanced - + Warning: Advanced usage only. - + Values too large may cause localdocs failure, extremely slow responses or failure to respond at all. Roughly speaking, the {N chars x N snippets} are added to the model's context window. More info <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">here</a>. - + Document snippet size (characters) - + Number of characters per document snippet. Larger numbers increase likelihood of factual responses, but also result in slower generation. - + Max document snippets per prompt - + Max best N matches of retrieved document snippets to add to the context for prompt. Larger numbers increase likelihood of factual responses, but also result in slower generation. @@ -1510,78 +1797,78 @@ model to get started ModelList - + <ul><li>Requires personal OpenAI API key.</li><li>WARNING: Will send your chats to OpenAI!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with OpenAI</li><li>You can apply for an API key <a href="https://platform.openai.com/account/api-keys">here.</a></li> - + <strong>OpenAI's ChatGPT model GPT-3.5 Turbo</strong><br> %1 - + <strong>OpenAI's ChatGPT model GPT-4</strong><br> %1 %2 - + <strong>Mistral Tiny model</strong><br> %1 - + <strong>Mistral Small model</strong><br> %1 - + <strong>Mistral Medium model</strong><br> %1 - + <br><br><i>* Even if you pay OpenAI for ChatGPT-4 this does not guarantee API key access. Contact OpenAI for more info. - - + + cannot open "%1": %2 - + cannot create "%1": %2 - + %1 (%2) - + <strong>OpenAI-Compatible API Model</strong><br><ul><li>API Key: %1</li><li>Base URL: %2</li><li>Model Name: %3</li></ul> - + <ul><li>Requires personal Mistral API key.</li><li>WARNING: Will send your chats to Mistral!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with Mistral</li><li>You can apply for an API key <a href="https://console.mistral.ai/user/api-keys">here</a>.</li> - + <ul><li>Requires personal API key and the API base URL.</li><li>WARNING: Will send your chats to the OpenAI-compatible API Server you specified!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with the OpenAI-compatible API Server</li> - + <strong>Connect to OpenAI-compatible API server</strong><br> %1 - + <strong>Created by %1.</strong><br><ul><li>Published on %2.<li>This model has %3 likes.<li>This model has %4 downloads.<li>More info can be found <a href="https://huggingface.co/%5">here.</a></ul> @@ -1594,212 +1881,280 @@ model to get started - + + %1 system message? + + + + + + Clear + + + + + + Reset + + + + + The system message will be %1. + + + + + removed + + + + + + reset to the default + + + + + %1 chat template? + + + + + The chat template will be %1. + + + + + erased + + + + Model Settings - + Clone - + Remove - + Name - + Model File - - System Prompt + + System Message + + + + + A message to set the context or guide the behavior of the model. Leave blank for none. NOTE: Since GPT4All 3.5, this should not contain control tokens. + + + + + System message is not <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">plain text</a>. + + + + + Chat Template - - Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens. + + This Jinja template turns the chat into input for the model. - - Prompt Template + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. - - The template that wraps every prompt. + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. - - Must contain the string "%1" to be replaced with the user's input. + + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Syntax error</a>: %1 - + + Chat template is not in <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Jinja format</a>. + + + + Chat Name Prompt - + Prompt used to automatically generate chat names. - + Suggested FollowUp Prompt - + Prompt used to generate suggested follow-up questions. - + Context Length - + Number of input and output tokens the model sees. - + Maximum combined prompt/response tokens before information is lost. Using more context than the model was trained on will yield poor results. NOTE: Does not take effect until you reload the model. - + Temperature - + Randomness of model output. Higher -> more variation. - + Temperature increases the chances of choosing less likely tokens. NOTE: Higher temperature gives more creative but less predictable outputs. - + Top-P - + Nucleus Sampling factor. Lower -> more predictable. - + Only the most likely tokens up to a total probability of top_p can be chosen. NOTE: Prevents choosing highly unlikely tokens. - + Min-P - + Minimum token probability. Higher -> more predictable. - + Sets the minimum relative probability for a token to be considered. - + Top-K - + Size of selection pool for tokens. - + Only the top K most likely tokens will be chosen from. - + Max Length - + Maximum response length, in tokens. - + Prompt Batch Size - + The batch size used for prompt processing. - + Amount of prompt tokens to process at once. NOTE: Higher values can speed up reading prompts but will use more RAM. - + Repeat Penalty - + Repetition penalty factor. Set to 1 to disable. - + Repeat Penalty Tokens - + Number of previous tokens used for penalty. - + GPU Layers - + Number of model layers to load into VRAM. - + How many model layers to load into VRAM. Decrease this if GPT4All runs out of VRAM while loading this model. Lower values increase CPU load and RAM usage, and make inference slower. NOTE: Does not take effect until you reload the model. @@ -2053,15 +2408,38 @@ NOTE: Does not take effect until you reload the model. + + MySettingsLabel + + + Clear + + + + + Reset + + + MySettingsTab - + + Restore defaults? + + + + + This page of settings will be reset to the defaults. + + + + Restore Defaults - + Restores settings dialog to a default state @@ -2246,6 +2624,11 @@ model release that uses your data! Describes what will happen when you opt-in + + + Opt-in to anonymous usage analytics used to improve GPT4All + + @@ -2279,6 +2662,11 @@ model release that uses your data! Allow opt-out for anonymous usage statistics + + + Opt-in to anonymous sharing of chats to the GPT4All Datalake + + @@ -2306,30 +2694,6 @@ model release that uses your data! - - SwitchModelDialog - - - <b>Warning:</b> changing the model will erase the current conversation. Do you wish to continue? - - - - - Continue - - - - - Continue with model loading - - - - - - Cancel - - - ThumbsDownDialog @@ -2366,125 +2730,135 @@ model release that uses your data! main - + <h3>Encountered an error starting up:</h3><br><i>"Incompatible hardware detected."</i><br><br>Unfortunately, your CPU does not meet the minimal requirements to run this program. In particular, it does not support AVX intrinsics which this program requires to successfully run a modern large language model. The only solution at this time is to upgrade your hardware to a more modern CPU.<br><br>See here for more information: <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">https://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> - + GPT4All v%1 - + + Restore + + + + + Quit + + + + <h3>Encountered an error starting up:</h3><br><i>"Inability to access settings file."</i><br><br>Unfortunately, something is preventing the program from accessing the settings file. This could be caused by incorrect permissions in the local app config directory where the settings file is located. Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help. - + Connection to datalake failed. - + Saving chats. - + Network dialog - + opt-in to share feedback/conversations - + Home view - + Home view of application - + Home - + Chat view - + Chat view to interact with models - + Chats - - + + Models - + Models view for installed models - - + + LocalDocs - + LocalDocs view to configure and use local docs - - + + Settings - + Settings view for application configuration - + The datalake is enabled - + Using a network model - + Server mode is enabled - + Installed models - + View of installed models diff --git a/gpt4all-chat/translations/gpt4all_es_MX.ts b/gpt4all-chat/translations/gpt4all_es_MX.ts index e7693bf23f0d..8112b7149df5 100644 --- a/gpt4all-chat/translations/gpt4all_es_MX.ts +++ b/gpt4all-chat/translations/gpt4all_es_MX.ts @@ -63,6 +63,467 @@ Crear colección + + AddGPT4AllModelView + + + These models have been specifically configured for use in GPT4All. The first few models on the list are known to work the best, but you should only attempt to use models that will fit in your available memory. + + + + + Network error: could not retrieve %1 + Error de red: no se pudo recuperar %1 + + + + + Busy indicator + Indicador de ocupado + + + + Displayed when the models request is ongoing + Se muestra cuando la solicitud de modelos está en curso + + + + Model file + Archivo del modelo + + + + Model file to be downloaded + Archivo del modelo a descargar + + + + Description + Descripción + + + + File description + Descripción del archivo + + + + Cancel + Cancelar + + + + Resume + Reanudar + + + + Download + Descargar + + + + Stop/restart/start the download + Detener/reiniciar/iniciar la descarga + + + + Remove + Eliminar + + + + Remove model from filesystem + Eliminar modelo del sistema de archivos + + + + + Install + Instalar + + + + Install online model + Instalar modelo en línea + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + <strong><font size="1"><a href="#error">Error</a></strong></font> + + + + Describes an error that occurred when downloading + Describe un error que ocurrió durante la descarga + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + + + + + Error for incompatible hardware + Error por hardware incompatible + + + + Download progressBar + Barra de progreso de descarga + + + + Shows the progress made in the download + Muestra el progreso realizado en la descarga + + + + Download speed + Velocidad de descarga + + + + Download speed in bytes/kilobytes/megabytes per second + Velocidad de descarga en bytes/kilobytes/megabytes por segundo + + + + Calculating... + Calculando... + + + + + + + Whether the file hash is being calculated + Si se está calculando el hash del archivo + + + + Displayed when the file hash is being calculated + Se muestra cuando se está calculando el hash del archivo + + + + ERROR: $API_KEY is empty. + + + + + enter $API_KEY + ingrese $API_KEY + + + + ERROR: $BASE_URL is empty. + + + + + enter $BASE_URL + ingrese $BASE_URL + + + + ERROR: $MODEL_NAME is empty. + ERROR: $MODEL_NAME está vacío. + + + + enter $MODEL_NAME + ingrese $MODEL_NAME + + + + File size + Tamaño del archivo + + + + RAM required + RAM requerida + + + + %1 GB + %1 GB + + + + + ? + ? + + + + Parameters + Parámetros + + + + Quant + Cuantificación + + + + Type + Tipo + + + + AddHFModelView + + + Use the search to find and download models from HuggingFace. There is NO GUARANTEE that these will work. Many will require additional configuration before they can be used. + + + + + Discover and download models by keyword search... + Descubre y descarga modelos mediante búsqueda por palabras clave... + + + + Text field for discovering and filtering downloadable models + Campo de texto para descubrir y filtrar modelos descargables + + + + Searching · %1 + Buscando · %1 + + + + Initiate model discovery and filtering + Iniciar descubrimiento y filtrado de modelos + + + + Triggers discovery and filtering of models + Activa el descubrimiento y filtrado de modelos + + + + Default + Predeterminado + + + + Likes + Me gusta + + + + Downloads + Descargas + + + + Recent + Reciente + + + + Sort by: %1 + Ordenar por: %1 + + + + Asc + Asc + + + + Desc + Desc + + + + Sort dir: %1 + Dirección de ordenamiento: %1 + + + + None + Ninguno + + + + Limit: %1 + Límite: %1 + + + + Model file + Archivo del modelo + + + + Model file to be downloaded + Archivo del modelo a descargar + + + + Description + Descripción + + + + File description + Descripción del archivo + + + + Cancel + Cancelar + + + + Resume + Reanudar + + + + Download + Descargar + + + + Stop/restart/start the download + Detener/reiniciar/iniciar la descarga + + + + Remove + Eliminar + + + + Remove model from filesystem + Eliminar modelo del sistema de archivos + + + + + Install + Instalar + + + + Install online model + Instalar modelo en línea + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + <strong><font size="1"><a href="#error">Error</a></strong></font> + + + + Describes an error that occurred when downloading + Describe un error que ocurrió durante la descarga + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + + + + + Error for incompatible hardware + Error por hardware incompatible + + + + Download progressBar + Barra de progreso de descarga + + + + Shows the progress made in the download + Muestra el progreso realizado en la descarga + + + + Download speed + Velocidad de descarga + + + + Download speed in bytes/kilobytes/megabytes per second + Velocidad de descarga en bytes/kilobytes/megabytes por segundo + + + + Calculating... + Calculando... + + + + + + + Whether the file hash is being calculated + Si se está calculando el hash del archivo + + + + Busy indicator + Indicador de ocupado + + + + Displayed when the file hash is being calculated + Se muestra cuando se está calculando el hash del archivo + + + + ERROR: $API_KEY is empty. + + + + + enter $API_KEY + ingrese $API_KEY + + + + ERROR: $BASE_URL is empty. + + + + + enter $BASE_URL + ingrese $BASE_URL + + + + ERROR: $MODEL_NAME is empty. + ERROR: $MODEL_NAME está vacío. + + + + enter $MODEL_NAME + ingrese $MODEL_NAME + + + + File size + Tamaño del archivo + + + + Quant + Cuantificación + + + + Type + Tipo + + AddModelView @@ -76,280 +537,230 @@ Explorar modelos - + + GPT4All + GPT4All + + + + HuggingFace + + + Discover and download models by keyword search... - Descubre y descarga modelos mediante búsqueda por palabras clave... + Descubre y descarga modelos mediante búsqueda por palabras clave... - Text field for discovering and filtering downloadable models - Campo de texto para descubrir y filtrar modelos descargables + Campo de texto para descubrir y filtrar modelos descargables - Initiate model discovery and filtering - Iniciar descubrimiento y filtrado de modelos + Iniciar descubrimiento y filtrado de modelos - Triggers discovery and filtering of models - Activa el descubrimiento y filtrado de modelos + Activa el descubrimiento y filtrado de modelos - Default - Predeterminado + Predeterminado - Likes - Me gusta + Me gusta - Downloads - Descargas + Descargas - Recent - Reciente + Reciente - Asc - Asc + Asc - Desc - Desc + Desc - None - Ninguno + Ninguno - Searching · %1 - Buscando · %1 + Buscando · %1 - Sort by: %1 - Ordenar por: %1 + Ordenar por: %1 - Sort dir: %1 - Dirección de ordenamiento: %1 + Dirección de ordenamiento: %1 - Limit: %1 - Límite: %1 + Límite: %1 - Network error: could not retrieve %1 - Error de red: no se pudo recuperar %1 + Error de red: no se pudo recuperar %1 - - Busy indicator - Indicador de ocupado + Indicador de ocupado - Displayed when the models request is ongoing - Se muestra cuando la solicitud de modelos está en curso + Se muestra cuando la solicitud de modelos está en curso - Model file - Archivo del modelo + Archivo del modelo - Model file to be downloaded - Archivo del modelo a descargar + Archivo del modelo a descargar - Description - Descripción + Descripción - File description - Descripción del archivo + Descripción del archivo - Cancel - Cancelar + Cancelar - Resume - Reanudar + Reanudar - Download - Descargar + Descargar - Stop/restart/start the download - Detener/reiniciar/iniciar la descarga + Detener/reiniciar/iniciar la descarga - Remove - Eliminar + Eliminar - Remove model from filesystem - Eliminar modelo del sistema de archivos + Eliminar modelo del sistema de archivos - - Install - Instalar + Instalar - Install online model - Instalar modelo en línea + Instalar modelo en línea - <strong><font size="1"><a href="#error">Error</a></strong></font> - <strong><font size="1"><a href="#error">Error</a></strong></font> + <strong><font size="1"><a href="#error">Error</a></strong></font> - <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> - <strong><font size="2">ADVERTENCIA: No recomendado para tu hardware. El modelo requiere más memoria (%1 GB) de la que tu sistema tiene disponible (%2).</strong></font> + <strong><font size="2">ADVERTENCIA: No recomendado para tu hardware. El modelo requiere más memoria (%1 GB) de la que tu sistema tiene disponible (%2).</strong></font> - %1 GB - %1 GB + %1 GB - - ? - ? + ? - Describes an error that occurred when downloading - Describe un error que ocurrió durante la descarga + Describe un error que ocurrió durante la descarga - Error for incompatible hardware - Error por hardware incompatible + Error por hardware incompatible - Download progressBar - Barra de progreso de descarga + Barra de progreso de descarga - Shows the progress made in the download - Muestra el progreso realizado en la descarga + Muestra el progreso realizado en la descarga - Download speed - Velocidad de descarga + Velocidad de descarga - Download speed in bytes/kilobytes/megabytes per second - Velocidad de descarga en bytes/kilobytes/megabytes por segundo + Velocidad de descarga en bytes/kilobytes/megabytes por segundo - Calculating... - Calculando... + Calculando... - - - - Whether the file hash is being calculated - Si se está calculando el hash del archivo + Si se está calculando el hash del archivo - Displayed when the file hash is being calculated - Se muestra cuando se está calculando el hash del archivo + Se muestra cuando se está calculando el hash del archivo - enter $API_KEY - ingrese $API_KEY + ingrese $API_KEY - File size - Tamaño del archivo + Tamaño del archivo - RAM required - RAM requerida + RAM requerida - Parameters - Parámetros + Parámetros - Quant - Cuantificación + Cuantificación - Type - Tipo + Tipo - ERROR: $API_KEY is empty. - ERROR: $API_KEY está vacío. + ERROR: $API_KEY está vacío. - ERROR: $BASE_URL is empty. - ERROR: $BASE_URL está vacío. + ERROR: $BASE_URL está vacío. - enter $BASE_URL - ingrese $BASE_URL + ingrese $BASE_URL - ERROR: $MODEL_NAME is empty. - ERROR: $MODEL_NAME está vacío. + ERROR: $MODEL_NAME está vacío. - enter $MODEL_NAME - ingrese $MODEL_NAME + ingrese $MODEL_NAME @@ -536,13 +947,21 @@ - Save Chat Context - Guardar contexto del chat + Enable System Tray + + The application will minimize to the system tray when the window is closed. + + + + Save Chat Context + Guardar contexto del chat + + Save the chat model's state to disk for faster loading. WARNING: Uses ~2GB per chat. - Guardar el estado del modelo de chat en el disco para una carga más rápida. ADVERTENCIA: Usa ~2GB por chat. + Guardar el estado del modelo de chat en el disco para una carga más rápida. ADVERTENCIA: Usa ~2GB por chat. @@ -599,13 +1018,13 @@ Chat - - + + New Chat Nuevo chat - + Server Chat Chat del servidor @@ -613,12 +1032,12 @@ ChatAPIWorker - + ERROR: Network error occurred while connecting to the API server ERROR: Ocurrió un error de red al conectar con el servidor API - + ChatAPIWorker::handleFinished got HTTP Error %1 %2 ChatAPIWorker::handleFinished obtuvo Error HTTP %1 %2 @@ -686,35 +1105,181 @@ Lista de chats en el diálogo del cajón + + ChatItemView + + + GPT4All + GPT4All + + + + You + + + + + response stopped ... + respuesta detenida ... + + + + retrieving localdocs: %1 ... + recuperando documentos locales: %1 ... + + + + searching localdocs: %1 ... + buscando en documentos locales: %1 ... + + + + processing ... + procesando ... + + + + generating response ... + generando respuesta ... + + + + generating questions ... + generando preguntas ... + + + + + Copy + Copiar + + + + Copy Message + Copiar mensaje + + + + Disable markdown + Desactivar markdown + + + + Enable markdown + Activar markdown + + + + %n Source(s) + + %n Fuente + %n Fuentes + + + + + LocalDocs + + + + + Edit this message? + + + + + + All following messages will be permanently erased. + + + + + Redo this response? + + + + + Cannot edit chat without a loaded model. + + + + + Cannot edit chat while the model is generating. + + + + + Edit + + + + + Cannot redo response without a loaded model. + + + + + Cannot redo response while the model is generating. + + + + + Redo + + + + + Like response + + + + + Dislike response + + + + + Suggested follow-ups + Seguimientos sugeridos + + + + ChatLLM + + + Your message was too long and could not be processed (%1 > %2). Please try again with something shorter. + + + ChatListModel - + TODAY HOY - + THIS WEEK ESTA SEMANA - + THIS MONTH ESTE MES - + LAST SIX MONTHS ÚLTIMOS SEIS MESES - + THIS YEAR ESTE AÑO - + LAST YEAR AÑO PASADO @@ -722,343 +1287,357 @@ ChatView - + <h3>Warning</h3><p>%1</p> <h3>Advertencia</h3><p>%1</p> - Switch model dialog - Diálogo para cambiar de modelo + Diálogo para cambiar de modelo - Warn the user if they switch models, then context will be erased - Advertir al usuario si cambia de modelo, entonces se borrará el contexto + Advertir al usuario si cambia de modelo, entonces se borrará el contexto - + Conversation copied to clipboard. Conversación copiada al portapapeles. - + Code copied to clipboard. Código copiado al portapapeles. - + + The entire chat will be erased. + + + + Chat panel Panel de chat - + Chat panel with options Panel de chat con opciones - + Reload the currently loaded model Recargar el modelo actualmente cargado - + Eject the currently loaded model Expulsar el modelo actualmente cargado - + No model installed. No hay modelo instalado. - + Model loading error. Error al cargar el modelo. - + Waiting for model... Esperando al modelo... - + Switching context... Cambiando contexto... - + Choose a model... Elige un modelo... - + Not found: %1 No encontrado: %1 - + The top item is the current model El elemento superior es el modelo actual - - + LocalDocs DocumentosLocales - + Add documents Agregar documentos - + add collections of documents to the chat agregar colecciones de documentos al chat - + Load the default model Cargar el modelo predeterminado - + Loads the default model which can be changed in settings Carga el modelo predeterminado que se puede cambiar en la configuración - + No Model Installed No hay modelo instalado - + + Legacy prompt template needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + + + + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + + + + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + + + + + Legacy system prompt needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + + + + <h3>Encountered an error loading model:</h3><br><i>"%1"</i><br><br>Model loading failures can happen for a variety of reasons, but the most common causes include a bad file format, an incomplete or corrupted download, the wrong file type, not enough system RAM or an incompatible model type. Here are some suggestions for resolving the problem:<br><ul><li>Ensure the model file has a compatible format and type<li>Check the model file is complete in the download folder<li>You can find the download folder in the settings dialog<li>If you've sideloaded the model ensure the file is not corrupt by checking md5sum<li>Read more about what models are supported in our <a href="https://docs.gpt4all.io/">documentation</a> for the gui<li>Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help <h3>Se encontró un error al cargar el modelo:</h3><br><i>"%1"</i><br><br>Los fallos en la carga de modelos pueden ocurrir por varias razones, pero las causas más comunes incluyen un formato de archivo incorrecto, una descarga incompleta o corrupta, un tipo de archivo equivocado, RAM del sistema insuficiente o un tipo de modelo incompatible. Aquí hay algunas sugerencias para resolver el problema:<br><ul><li>Asegúrate de que el archivo del modelo tenga un formato y tipo compatibles<li>Verifica que el archivo del modelo esté completo en la carpeta de descargas<li>Puedes encontrar la carpeta de descargas en el diálogo de configuración<li>Si has cargado el modelo manualmente, asegúrate de que el archivo no esté corrupto verificando el md5sum<li>Lee más sobre qué modelos son compatibles en nuestra <a href="https://docs.gpt4all.io/">documentación</a> para la interfaz gráfica<li>Visita nuestro <a href="https://discord.gg/4M2QFmTt2k">canal de discord</a> para obtener ayuda - + + + Erase conversation? + + + + + Changing the model will erase the current conversation. + + + + Install a Model Instalar un modelo - + Shows the add model view Muestra la vista de agregar modelo - + Conversation with the model Conversación con el modelo - + prompt / response pairs from the conversation pares de pregunta / respuesta de la conversación - GPT4All - GPT4All + GPT4All - You - + - response stopped ... - respuesta detenida ... + respuesta detenida ... - processing ... - procesando ... + procesando ... - generating response ... - generando respuesta ... + generando respuesta ... - generating questions ... - generando preguntas ... + generando preguntas ... - - + Copy Copiar - Copy Message - Copiar mensaje + Copiar mensaje - Disable markdown - Desactivar markdown + Desactivar markdown - Enable markdown - Activar markdown + Activar markdown - Thumbs up - Me gusta + Me gusta - Gives a thumbs up to the response - Da un me gusta a la respuesta + Da un me gusta a la respuesta - Thumbs down - No me gusta + No me gusta - Opens thumbs down dialog - Abre el diálogo de no me gusta + Abre el diálogo de no me gusta - Suggested follow-ups - Seguimientos sugeridos + Seguimientos sugeridos - + Erase and reset chat session Borrar y reiniciar sesión de chat - + Copy chat session to clipboard Copiar sesión de chat al portapapeles - Redo last chat response - Rehacer última respuesta del chat + Rehacer última respuesta del chat - + Add media Agregar medios - + Adds media to the prompt Agrega medios al mensaje - + Stop generating Detener generación - + Stop the current response generation Detener la generación de la respuesta actual - + Attach Adjuntar - + Single File Fila india - + Reloads the model Recarga el modelo - - + + Reload · %1 Recargar · %1 - + Loading · %1 Cargando · %1 - + Load · %1 (default) → Cargar · %1 (predeterminado) → - retrieving localdocs: %1 ... - recuperando documentos locales: %1 ... + recuperando documentos locales: %1 ... - searching localdocs: %1 ... - buscando en documentos locales: %1 ... + buscando en documentos locales: %1 ... - %n Source(s) - + %n Fuente %n Fuentes - + Send a message... Enviar un mensaje... - + Load a model to continue... Carga un modelo para continuar... - + Send messages/prompts to the model Enviar mensajes/indicaciones al modelo - + Cut Cortar - + Paste Pegar - + Select All Seleccionar todo - + Send message Enviar mensaje - + Sends the message/prompt contained in textfield to the model Envía el mensaje/indicación contenido en el campo de texto al modelo - + GPT4All requires that you install at least one model to get started GPT4All requiere que instale al menos un @@ -1066,9 +1645,8 @@ modelo para comenzar - restoring from text ... - restaurando desde texto ... + restaurando desde texto ... @@ -1110,40 +1688,53 @@ modelo para comenzar Seleccione una colección para hacerla disponible al modelo de chat. + + ConfirmationDialog + + + OK + + + + + Cancel + Cancelar + + Download - + Model "%1" is installed successfully. El modelo "%1" se ha instalado correctamente. - + ERROR: $MODEL_NAME is empty. ERROR: $MODEL_NAME está vacío. - + ERROR: $API_KEY is empty. ERROR: $API_KEY está vacía. - + ERROR: $BASE_URL is invalid. ERROR: $BASE_URL no es válida. - + ERROR: Model "%1 (%2)" is conflict. ERROR: El modelo "%1 (%2)" está en conflicto. - + Model "%1 (%2)" is installed successfully. El modelo "%1 (%2)" se ha instalado correctamente. - + Model "%1" is removed. El modelo "%1" ha sido eliminado. @@ -1309,52 +1900,52 @@ modelo para comenzar Predeterminado de la aplicación - + Display Visualización - + Show Sources Mostrar fuentes - + Display the sources used for each response. Mostrar las fuentes utilizadas para cada respuesta. - + Advanced Avanzado - + Warning: Advanced usage only. Advertencia: Solo para uso avanzado. - + Values too large may cause localdocs failure, extremely slow responses or failure to respond at all. Roughly speaking, the {N chars x N snippets} are added to the model's context window. More info <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">here</a>. Valores demasiado grandes pueden causar fallos en localdocs, respuestas extremadamente lentas o falta de respuesta. En términos generales, los {N caracteres x N fragmentos} se añaden a la ventana de contexto del modelo. Más información <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">aquí</a>. - + Number of characters per document snippet. Larger numbers increase likelihood of factual responses, but also result in slower generation. Número de caracteres por fragmento de documento. Números más grandes aumentan la probabilidad de respuestas verídicas, pero también resultan en una generación más lenta. - + Max best N matches of retrieved document snippets to add to the context for prompt. Larger numbers increase likelihood of factual responses, but also result in slower generation. Máximo de N mejores coincidencias de fragmentos de documentos recuperados para añadir al contexto del prompt. Números más grandes aumentan la probabilidad de respuestas verídicas, pero también resultan en una generación más lenta. - + Document snippet size (characters) Tamaño del fragmento de documento (caracteres) - + Max document snippets per prompt Máximo de fragmentos de documento por indicación @@ -1516,78 +2107,78 @@ modelo para comenzar ModelList - + <ul><li>Requires personal OpenAI API key.</li><li>WARNING: Will send your chats to OpenAI!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with OpenAI</li><li>You can apply for an API key <a href="https://platform.openai.com/account/api-keys">here.</a></li> <ul><li>Requiere clave API personal de OpenAI.</li><li>ADVERTENCIA: ¡Enviará sus chats a OpenAI!</li><li>Su clave API se almacenará en el disco</li><li>Solo se usará para comunicarse con OpenAI</li><li>Puede solicitar una clave API <a href="https://platform.openai.com/account/api-keys">aquí.</a></li> - + <strong>OpenAI's ChatGPT model GPT-3.5 Turbo</strong><br> %1 <strong>Modelo ChatGPT GPT-3.5 Turbo de OpenAI</strong><br> %1 - + <br><br><i>* Even if you pay OpenAI for ChatGPT-4 this does not guarantee API key access. Contact OpenAI for more info. <br><br><i>* Aunque pagues a OpenAI por ChatGPT-4, esto no garantiza el acceso a la clave API. Contacta a OpenAI para más información. - + <strong>OpenAI's ChatGPT model GPT-4</strong><br> %1 %2 <strong>Modelo ChatGPT GPT-4 de OpenAI</strong><br> %1 %2 - + <ul><li>Requires personal Mistral API key.</li><li>WARNING: Will send your chats to Mistral!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with Mistral</li><li>You can apply for an API key <a href="https://console.mistral.ai/user/api-keys">here</a>.</li> <ul><li>Requiere una clave API personal de Mistral.</li><li>ADVERTENCIA: ¡Enviará tus chats a Mistral!</li><li>Tu clave API se almacenará en el disco</li><li>Solo se usará para comunicarse con Mistral</li><li>Puedes solicitar una clave API <a href="https://console.mistral.ai/user/api-keys">aquí</a>.</li> - + <strong>Mistral Tiny model</strong><br> %1 <strong>Modelo Mistral Tiny</strong><br> %1 - + <strong>Mistral Small model</strong><br> %1 <strong>Modelo Mistral Small</strong><br> %1 - + <strong>Mistral Medium model</strong><br> %1 <strong>Modelo Mistral Medium</strong><br> %1 - + <strong>Created by %1.</strong><br><ul><li>Published on %2.<li>This model has %3 likes.<li>This model has %4 downloads.<li>More info can be found <a href="https://huggingface.co/%5">here.</a></ul> <strong>Creado por %1.</strong><br><ul><li>Publicado el %2.<li>Este modelo tiene %3 me gusta.<li>Este modelo tiene %4 descargas.<li>Más información puede encontrarse <a href="https://huggingface.co/%5">aquí.</a></ul> - + %1 (%2) %1 (%2) - - + + cannot open "%1": %2 no se puede abrir "%1": %2 - + cannot create "%1": %2 no se puede crear "%1": %2 - + <strong>OpenAI-Compatible API Model</strong><br><ul><li>API Key: %1</li><li>Base URL: %2</li><li>Model Name: %3</li></ul> <strong>Modelo de API compatible con OpenAI</strong><br><ul><li>Clave API: %1</li><li>URL base: %2</li><li>Nombre del modelo: %3</li></ul> - + <ul><li>Requires personal API key and the API base URL.</li><li>WARNING: Will send your chats to the OpenAI-compatible API Server you specified!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with the OpenAI-compatible API Server</li> <ul><li>Requiere una clave API personal y la URL base de la API.</li><li>ADVERTENCIA: ¡Enviará sus chats al servidor de API compatible con OpenAI que especificó!</li><li>Su clave API se almacenará en el disco</li><li>Solo se utilizará para comunicarse con el servidor de API compatible con OpenAI</li> - + <strong>Connect to OpenAI-compatible API server</strong><br> %1 <strong>Conectar al servidor de API compatible con OpenAI</strong><br> %1 @@ -1600,208 +2191,251 @@ modelo para comenzar Modelo - + + %1 system message? + + + + + + Clear + + + + + + Reset + + + + + The system message will be %1. + + + + + removed + + + + + + reset to the default + + + + + %1 chat template? + + + + + The chat template will be %1. + + + + + erased + + + + Model Settings Configuración del modelo - + Clone Clonar - + Remove Eliminar - + Name Nombre - + Model File Archivo del modelo - System Prompt - Indicación del sistema + Indicación del sistema - Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens. - Prefijado al inicio de cada conversación. Debe contener los tokens de encuadre apropiados. + Prefijado al inicio de cada conversación. Debe contener los tokens de encuadre apropiados. - Prompt Template - Plantilla de indicación + Plantilla de indicación - The template that wraps every prompt. - La plantilla que envuelve cada indicación. + La plantilla que envuelve cada indicación. - Must contain the string "%1" to be replaced with the user's input. - Debe contener la cadena "%1" para ser reemplazada con la entrada del usuario. + Debe contener la cadena "%1" para ser reemplazada con la entrada del usuario. - + Chat Name Prompt Indicación para el nombre del chat - + Prompt used to automatically generate chat names. Indicación utilizada para generar automáticamente nombres de chat. - + Suggested FollowUp Prompt Indicación de seguimiento sugerida - + Prompt used to generate suggested follow-up questions. Indicación utilizada para generar preguntas de seguimiento sugeridas. - + Context Length Longitud del contexto - + Number of input and output tokens the model sees. Número de tokens de entrada y salida que el modelo ve. - + Temperature Temperatura - + Randomness of model output. Higher -> more variation. Aleatoriedad de la salida del modelo. Mayor -> más variación. - + Temperature increases the chances of choosing less likely tokens. NOTE: Higher temperature gives more creative but less predictable outputs. La temperatura aumenta las probabilidades de elegir tokens menos probables. NOTA: Una temperatura más alta da resultados más creativos pero menos predecibles. - + Top-P Top-P - + Nucleus Sampling factor. Lower -> more predictable. Factor de muestreo de núcleo. Menor -> más predecible. - + Only the most likely tokens up to a total probability of top_p can be chosen. NOTE: Prevents choosing highly unlikely tokens. Solo se pueden elegir los tokens más probables hasta una probabilidad total de top_p. NOTA: Evita elegir tokens altamente improbables. - + Min-P Min-P - + Minimum token probability. Higher -> more predictable. Probabilidad mínima del token. Mayor -> más predecible. - + Sets the minimum relative probability for a token to be considered. Establece la probabilidad relativa mínima para que un token sea considerado. - + Top-K Top-K - + Size of selection pool for tokens. Tamaño del grupo de selección para tokens. - + Only the top K most likely tokens will be chosen from. Solo se elegirán los K tokens más probables. - + Max Length Longitud máxima - + Maximum response length, in tokens. Longitud máxima de respuesta, en tokens. - + Prompt Batch Size Tamaño del lote de indicaciones - + The batch size used for prompt processing. El tamaño del lote utilizado para el procesamiento de indicaciones. - + Amount of prompt tokens to process at once. NOTE: Higher values can speed up reading prompts but will use more RAM. Cantidad de tokens de prompt a procesar de una vez. NOTA: Valores más altos pueden acelerar la lectura de prompts, pero usarán más RAM. - + Repeat Penalty Penalización por repetición - + Repetition penalty factor. Set to 1 to disable. Factor de penalización por repetición. Establecer a 1 para desactivar. - + Repeat Penalty Tokens Tokens de penalización por repetición - + Number of previous tokens used for penalty. Número de tokens anteriores utilizados para la penalización. - + GPU Layers Capas de GPU - + Number of model layers to load into VRAM. Número de capas del modelo a cargar en la VRAM. - + Maximum combined prompt/response tokens before information is lost. Using more context than the model was trained on will yield poor results. NOTE: Does not take effect until you reload the model. @@ -1810,7 +2444,52 @@ Usar más contexto del que el modelo fue entrenado producirá resultados deficie NOTA: No surtirá efecto hasta que recargue el modelo. - + + System Message + + + + + A message to set the context or guide the behavior of the model. Leave blank for none. NOTE: Since GPT4All 3.5, this should not contain control tokens. + + + + + System message is not <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">plain text</a>. + + + + + Chat Template + + + + + This Jinja template turns the chat into input for the model. + + + + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + + + + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + + + + + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Syntax error</a>: %1 + + + + + Chat template is not in <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Jinja format</a>. + + + + How many model layers to load into VRAM. Decrease this if GPT4All runs out of VRAM while loading this model. Lower values increase CPU load and RAM usage, and make inference slower. NOTE: Does not take effect until you reload the model. @@ -2066,6 +2745,19 @@ NOTA: No surte efecto hasta que recargue el modelo. Por favor, elija un directorio + + MySettingsLabel + + + Clear + + + + + Reset + + + MySettingsStack @@ -2076,12 +2768,22 @@ NOTA: No surte efecto hasta que recargue el modelo. MySettingsTab - + + Restore defaults? + + + + + This page of settings will be reset to the defaults. + + + + Restore Defaults Restaurar valores predeterminados - + Restores settings dialog to a default state Restaura el diálogo de configuración a su estado predeterminado @@ -2255,6 +2957,11 @@ NOTA: Al activar esta función, estarás enviando tus datos al Datalake de Códi Describes what will happen when you opt-in Describe lo que sucederá cuando acepte + + + Opt-in to anonymous usage analytics used to improve GPT4All + + @@ -2288,6 +2995,11 @@ NOTA: Al activar esta función, estarás enviando tus datos al Datalake de Códi Allow opt-out for anonymous usage statistics Permitir rechazo de estadísticas de uso anónimas + + + Opt-in to anonymous sharing of chats to the GPT4All Datalake + + @@ -2349,25 +3061,20 @@ lanzamiento de modelo GPT4All que utilice sus datos. SwitchModelDialog - <b>Warning:</b> changing the model will erase the current conversation. Do you wish to continue? - <b>Advertencia:</b> cambiar el modelo borrará la conversación actual. ¿Deseas continuar? + <b>Advertencia:</b> cambiar el modelo borrará la conversación actual. ¿Deseas continuar? - Continue - Continuar + Continuar - Continue with model loading - Continuar con la carga del modelo + Continuar con la carga del modelo - - Cancel - Cancelar + Cancelar @@ -2407,126 +3114,136 @@ lanzamiento de modelo GPT4All que utilice sus datos. main - + GPT4All v%1 GPT4All v%1 - + + Restore + + + + + Quit + + + + <h3>Encountered an error starting up:</h3><br><i>"Incompatible hardware detected."</i><br><br>Unfortunately, your CPU does not meet the minimal requirements to run this program. In particular, it does not support AVX intrinsics which this program requires to successfully run a modern large language model. The only solution at this time is to upgrade your hardware to a more modern CPU.<br><br>See here for more information: <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">https://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> <h3>Se encontró un error al iniciar:</h3><br><i>"Se detectó hardware incompatible."</i><br><br>Desafortunadamente, tu CPU no cumple con los requisitos mínimos para ejecutar este programa. En particular, no soporta instrucciones AVX, las cuales este programa requiere para ejecutar con éxito un modelo de lenguaje grande moderno. La única solución en este momento es actualizar tu hardware a una CPU más moderna.<br><br>Consulta aquí para más información: <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">https://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> - + <h3>Encountered an error starting up:</h3><br><i>"Inability to access settings file."</i><br><br>Unfortunately, something is preventing the program from accessing the settings file. This could be caused by incorrect permissions in the local app config directory where the settings file is located. Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help. <h3>Se encontró un error al iniciar:</h3><br><i>"No se puede acceder al archivo de configuración."</i><br><br>Desafortunadamente, algo está impidiendo que el programa acceda al archivo de configuración. Esto podría ser causado por permisos incorrectos en el directorio de configuración local de la aplicación donde se encuentra el archivo de configuración. Visita nuestro <a href="https://discord.gg/4M2QFmTt2k">canal de Discord</a> para obtener ayuda. - + Connection to datalake failed. La conexión al datalake falló. - + Saving chats. Guardando chats. - + Network dialog Diálogo de red - + opt-in to share feedback/conversations optar por compartir comentarios/conversaciones - + Home view Vista de inicio - + Home view of application Vista de inicio de la aplicación - + Home Inicio - + Chat view Vista de chat - + Chat view to interact with models Vista de chat para interactuar con modelos - + Chats Chats - - + + Models Modelos - + Models view for installed models Vista de modelos para modelos instalados - - + + LocalDocs Docs Locales - + LocalDocs view to configure and use local docs Vista de DocumentosLocales para configurar y usar documentos locales - - + + Settings Config. - + Settings view for application configuration Vista de configuración para la configuración de la aplicación - + The datalake is enabled El datalake está habilitado - + Using a network model Usando un modelo de red - + Server mode is enabled El modo servidor está habilitado - + Installed models Modelos instalados - + View of installed models Vista de modelos instalados diff --git a/gpt4all-chat/translations/gpt4all_it_IT.ts b/gpt4all-chat/translations/gpt4all_it_IT.ts index e2c810becc07..b360518d0c98 100644 --- a/gpt4all-chat/translations/gpt4all_it_IT.ts +++ b/gpt4all-chat/translations/gpt4all_it_IT.ts @@ -18,10 +18,6 @@ Add a folder containing plain text files, PDFs, or Markdown. Configure additional extensions in Settings. Aggiungi una cartella contenente file di testo semplice, PDF o Markdown. Configura estensioni aggiuntive in Settaggi. - - Please choose a directory - Scegli una cartella - Name @@ -64,292 +60,691 @@ - AddModelView + AddGPT4AllModelView - - ← Existing Models - ← Modelli esistenti + + These models have been specifically configured for use in GPT4All. The first few models on the list are known to work the best, but you should only attempt to use models that will fit in your available memory. + Questi modelli sono stati specificamente configurati per l'uso in GPT4All. I primi modelli dell'elenco sono noti per funzionare meglio, ma dovresti utilizzare solo modelli che possano rientrare nella memoria disponibile. - - Explore Models - Esplora modelli + + Network error: could not retrieve %1 + Errore di rete: impossibile recuperare %1 + + + + + Busy indicator + Indicatore di occupato + + + + Displayed when the models request is ongoing + Visualizzato quando la richiesta dei modelli è in corso + + + + Model file + File del modello + + + + Model file to be downloaded + File del modello da scaricare + + + + Description + Descrizione + + + + File description + Descrizione del file + + + + Cancel + Annulla + + + + Resume + Riprendi + + + + Download + Scarica + + + + Stop/restart/start the download + Arresta/riavvia/avvia il download + + + + Remove + Rimuovi + + + + Remove model from filesystem + Rimuovi il modello dal sistema dei file + + + + + Install + Installa + + + + Install online model + Installa il modello online + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + <strong><font size="1"><a href="#error">Errore</a></strong></font> + + + + Describes an error that occurred when downloading + Descrive un errore che si è verificato durante lo scaricamento + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + <strong><font size="2">AVVISO: non consigliato per il tuo hardware. Il modello richiede più memoria (%1 GB) di quella disponibile nel sistema (%2).</strong></font> + + + + Error for incompatible hardware + Errore per hardware incompatibile + + + + Download progressBar + Barra di avanzamento dello scaricamento + + + + Shows the progress made in the download + Mostra lo stato di avanzamento dello scaricamento + + + + Download speed + Velocità di scaricamento + + + + Download speed in bytes/kilobytes/megabytes per second + Velocità di scaricamento in byte/kilobyte/megabyte al secondo + + + + Calculating... + Calcolo in corso... + + + + + + + Whether the file hash is being calculated + Se viene calcolato l'hash del file + + + + Displayed when the file hash is being calculated + Visualizzato durante il calcolo dell'hash del file + + + + ERROR: $API_KEY is empty. + ERRORE: $API_KEY è vuoto. + + + + enter $API_KEY + Inserire $API_KEY + + + + ERROR: $BASE_URL is empty. + ERRORE: $BASE_URL non è valido. + + + + enter $BASE_URL + inserisci $BASE_URL + + + + ERROR: $MODEL_NAME is empty. + ERRORE: $MODEL_NAME è vuoto. + + + + enter $MODEL_NAME + inserisci $MODEL_NAME + + + + File size + Dimensione del file - + + RAM required + RAM richiesta + + + + %1 GB + %1 GB + + + + + ? + ? + + + + Parameters + Parametri + + + + Quant + Quant + + + + Type + Tipo + + + + AddHFModelView + + + Use the search to find and download models from HuggingFace. There is NO GUARANTEE that these will work. Many will require additional configuration before they can be used. + Usa la ricerca per trovare e scaricare modelli da HuggingFace. NON C'È ALCUNA GARANZIA che funzioneranno. Molti richiederanno configurazioni aggiuntive prima di poter essere utilizzati. + + + Discover and download models by keyword search... Scopri e scarica i modelli tramite ricerca per parole chiave... - + Text field for discovering and filtering downloadable models Campo di testo per scoprire e filtrare i modelli scaricabili - + + Searching · %1 + Ricerca · %1 + + + Initiate model discovery and filtering Avvia rilevamento e filtraggio dei modelli - + Triggers discovery and filtering of models Attiva la scoperta e il filtraggio dei modelli - + Default Predefinito - + Likes Mi piace - + Downloads Scaricamenti - + Recent Recenti - + + Sort by: %1 + Ordina per: %1 + + + Asc Asc - + Desc Disc - - None - Niente - - - - Searching · %1 - Ricerca · %1 - - - - Sort by: %1 - Ordina per: %1 - - - + Sort dir: %1 Direzione ordinamento: %1 - - Limit: %1 - Limite: %1 - - - - Network error: could not retrieve %1 - Errore di rete: impossibile recuperare %1 - - - - - Busy indicator - Indicatore di occupato + + None + Niente - - Displayed when the models request is ongoing - Visualizzato quando la richiesta dei modelli è in corso + + Limit: %1 + Limite: %1 - + Model file File del modello - + Model file to be downloaded File del modello da scaricare - + Description Descrizione - + File description Descrizione del file - + Cancel Annulla - + Resume Riprendi - + Download Scarica - + Stop/restart/start the download Arresta/riavvia/avvia il download - + Remove Rimuovi - + Remove model from filesystem Rimuovi il modello dal sistema dei file - - + + Install Installa - + Install online model Installa il modello online - + + <strong><font size="1"><a href="#error">Error</a></strong></font> + <strong><font size="1"><a href="#error">Errore</a></strong></font> + + + + Describes an error that occurred when downloading + Descrive un errore che si è verificato durante lo scaricamento + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> - <strong><font size="2">AVVERTENZA: non consigliato per il tuo hardware. Il modello richiede più memoria (%1 GB) di quella disponibile nel sistema (%2).</strong></font> + <strong><font size="2">AVVISO: non consigliato per il tuo hardware. Il modello richiede più memoria (%1 GB) di quella disponibile nel sistema (%2).</strong></font> + + + + Error for incompatible hardware + Errore per hardware incompatibile + + + + Download progressBar + Barra di avanzamento dello scaricamento + + + + Shows the progress made in the download + Mostra lo stato di avanzamento dello scaricamento + + + + Download speed + Velocità di scaricamento + + + + Download speed in bytes/kilobytes/megabytes per second + Velocità di scaricamento in byte/kilobyte/megabyte al secondo + + + + Calculating... + Calcolo in corso... - + + + + + Whether the file hash is being calculated + Se viene calcolato l'hash del file + + + + Busy indicator + Indicatore di occupato + + + + Displayed when the file hash is being calculated + Visualizzato durante il calcolo dell'hash del file + + + ERROR: $API_KEY is empty. ERRORE: $API_KEY è vuoto. - + + enter $API_KEY + Inserire $API_KEY + + + ERROR: $BASE_URL is empty. ERRORE: $BASE_URL non è valido. - + enter $BASE_URL inserisci $BASE_URL - + ERROR: $MODEL_NAME is empty. ERRORE: $MODEL_NAME è vuoto. - + enter $MODEL_NAME inserisci $MODEL_NAME - - %1 GB - + + File size + Dimensione del file + + + + Quant + Quant + + + + Type + Tipo + + + + AddModelView + + + ← Existing Models + ← Modelli esistenti + + + + Explore Models + Esplora modelli + + + + GPT4All + GPT4All + + + + HuggingFace + HuggingFace + + + Discover and download models by keyword search... + Scopri e scarica i modelli tramite ricerca per parole chiave... + + + Text field for discovering and filtering downloadable models + Campo di testo per scoprire e filtrare i modelli scaricabili + + + Initiate model discovery and filtering + Avvia rilevamento e filtraggio dei modelli + + + Triggers discovery and filtering of models + Attiva la scoperta e il filtraggio dei modelli + + + Default + Predefinito + + + Likes + Mi piace + + + Downloads + Scaricamenti + + + Recent + Recenti + + + Asc + Asc + + + Desc + Disc + + + None + Niente + + + Searching · %1 + Ricerca · %1 + + + Sort by: %1 + Ordina per: %1 + + + Sort dir: %1 + Direzione ordinamento: %1 + + + Limit: %1 + Limite: %1 + + + Network error: could not retrieve %1 + Errore di rete: impossibile recuperare %1 + + + Busy indicator + Indicatore di occupato + + + Displayed when the models request is ongoing + Visualizzato quando la richiesta dei modelli è in corso + + + Model file + File del modello + + + Model file to be downloaded + File del modello da scaricare + + + Description + Descrizione + + + File description + Descrizione del file + + + Cancel + Annulla + + + Resume + Riprendi + + + Download + Scarica + + + Stop/restart/start the download + Arresta/riavvia/avvia il download + + + Remove + Rimuovi + + + Remove model from filesystem + Rimuovi il modello dal sistema dei file + + + Install + Installa + + + Install online model + Installa il modello online + + + ERROR: $API_KEY is empty. + ERRORE: $API_KEY è vuoto. + + + ERROR: $BASE_URL is empty. + ERRORE: $BASE_URL non è valido. + + + enter $BASE_URL + inserisci $BASE_URL + + + ERROR: $MODEL_NAME is empty. + ERRORE: $MODEL_NAME è vuoto. - - - ? - + enter $MODEL_NAME + inserisci $MODEL_NAME - Describes an error that occurred when downloading - Descrive un errore che si è verificato durante lo scaricamento + Descrive un errore che si è verificato durante lo scaricamento - <strong><font size="1"><a href="#error">Error</a></strong></font> - <strong><font size="1"><a href="#error">Errore</a></strong></font> + <strong><font size="1"><a href="#error">Errore</a></strong></font> - Error for incompatible hardware - Errore per hardware incompatibile + Errore per hardware incompatibile - Download progressBar - Barra di avanzamento dello scaricamento + Barra di avanzamento dello scaricamento - Shows the progress made in the download - Mostra lo stato di avanzamento dello scaricamento + Mostra lo stato di avanzamento dello scaricamento - Download speed - Velocità di scaricamento + Velocità di scaricamento - Download speed in bytes/kilobytes/megabytes per second - Velocità di scaricamento in byte/kilobyte/megabyte al secondo + Velocità di scaricamento in byte/kilobyte/megabyte al secondo - Calculating... - Calcolo in corso... + Calcolo in corso... - - - - Whether the file hash is being calculated - Se viene calcolato l'hash del file + Se viene calcolato l'hash del file - Displayed when the file hash is being calculated - Visualizzato durante il calcolo dell'hash del file + Visualizzato durante il calcolo dell'hash del file - enter $API_KEY - Inserire $API_KEY + Inserire $API_KEY - File size - Dimensione del file + Dimensione del file - RAM required - RAM richiesta + RAM richiesta - Parameters - Parametri + Parametri - Quant - Quant + Quant - Type - Tipo + Tipo @@ -459,10 +854,6 @@ Device Dispositivo - - The compute device used for text generation. "Auto" uses Vulkan or Metal. - Il dispositivo di calcolo utilizzato per la generazione del testo. "Auto" utilizza Vulkan o Metal. - The compute device used for text generation. @@ -557,23 +948,19 @@ - Save Chat Context - Salva il contesto della chat + Enable System Tray + Abilita la barra delle applicazioni - Save the chat model's state to disk for faster loading. WARNING: Uses ~2GB per chat. - Salva lo stato del modello di chat su disco per un caricamento più rapido. ATTENZIONE: utilizza circa 2 GB per chat. + The application will minimize to the system tray when the window is closed. + Quando la finestra viene chiusa, l'applicazione verrà ridotta a icona nella barra delle applicazioni. Enable Local API Server Abilita il server API locale - - Enable Local Server - Abilita server locale - Expose an OpenAI-Compatible server to localhost. WARNING: Results in increased resource usage. @@ -608,13 +995,13 @@ Chat - - + + New Chat Nuova Chat - + Server Chat Chat del server @@ -622,12 +1009,12 @@ ChatAPIWorker - + ERROR: Network error occurred while connecting to the API server ERRORE: si è verificato un errore di rete durante la connessione al server API - + ChatAPIWorker::handleFinished got HTTP Error %1 %2 ChatAPIWorker::handleFinished ha ricevuto l'errore HTTP %1 %2 @@ -695,35 +1082,181 @@ Elenco delle chat nella finestra di dialogo del cassetto + + ChatItemView + + + GPT4All + + + + + You + Tu + + + + response stopped ... + risposta interrotta ... + + + + retrieving localdocs: %1 ... + recupero documenti locali: %1 ... + + + + searching localdocs: %1 ... + ricerca in documenti locali: %1 ... + + + + processing ... + elaborazione ... + + + + generating response ... + generazione risposta ... + + + + generating questions ... + generazione domande ... + + + + + Copy + Copia + + + + Copy Message + Copia messaggio + + + + Disable markdown + Disabilita Markdown + + + + Enable markdown + Abilita Markdown + + + + %n Source(s) + + %n Fonte + %n Fonti + + + + + LocalDocs + + + + + Edit this message? + Vuoi modificare questo messaggio? + + + + + All following messages will be permanently erased. + Tutti i messaggi successivi verranno cancellati definitivamente. + + + + Redo this response? + Ripetere questa risposta? + + + + Cannot edit chat without a loaded model. + Non è possibile modificare la chat senza un modello caricato. + + + + Cannot edit chat while the model is generating. + Impossibile modificare la chat mentre il modello è in fase di generazione. + + + + Edit + Modifica + + + + Cannot redo response without a loaded model. + Non è possibile ripetere la risposta senza un modello caricato. + + + + Cannot redo response while the model is generating. + Impossibile ripetere la risposta mentre il modello è in fase di generazione. + + + + Redo + Ripeti + + + + Like response + Mi piace la risposta + + + + Dislike response + Non mi piace la risposta + + + + Suggested follow-ups + Approfondimenti suggeriti + + + + ChatLLM + + + Your message was too long and could not be processed (%1 > %2). Please try again with something shorter. + Il messaggio era troppo lungo e non è stato possibile elaborarlo (%1 > %2). Riprova con un messaggio più breve. + + ChatListModel - + TODAY OGGI - + THIS WEEK QUESTA SETTIMANA - + THIS MONTH QUESTO MESE - + LAST SIX MONTHS ULTIMI SEI MESI - + THIS YEAR QUEST'ANNO - + LAST YEAR L'ANNO SCORSO @@ -731,350 +1264,276 @@ ChatView - + <h3>Warning</h3><p>%1</p> <h3>Avviso</h3><p>%1</p> - - Switch model dialog - Finestra di dialogo Cambia modello - - - - Warn the user if they switch models, then context will be erased - Avvisa l'utente che se cambia modello, il contesto verrà cancellato - - - + Conversation copied to clipboard. Conversazione copiata negli appunti. - + Code copied to clipboard. Codice copiato negli appunti. - + + The entire chat will be erased. + L'intera chat verrà cancellata. + + + Chat panel Pannello chat - + Chat panel with options Pannello chat con opzioni - + Reload the currently loaded model Ricarica il modello attualmente caricato - + Eject the currently loaded model Espelli il modello attualmente caricato - + No model installed. Nessun modello installato. - + Model loading error. Errore di caricamento del modello. - + Waiting for model... In attesa del modello... - + Switching context... Cambio contesto... - + Choose a model... Scegli un modello... - + Not found: %1 Non trovato: %1 - + The top item is the current model L'elemento in alto è il modello attuale - - + LocalDocs - + Add documents Aggiungi documenti - + add collections of documents to the chat aggiungi raccolte di documenti alla chat - + Load the default model Carica il modello predefinito - + Loads the default model which can be changed in settings Carica il modello predefinito che può essere modificato nei settaggi - + No Model Installed Nessun modello installato - + GPT4All requires that you install at least one model to get started GPT4All richiede l'installazione di almeno un modello per iniziare - + Install a Model Installa un modello - + Shows the add model view Mostra la vista aggiungi modello - + Conversation with the model Conversazione con il modello - + prompt / response pairs from the conversation coppie prompt/risposta dalla conversazione - - GPT4All - - - - - You - Tu - - - - response stopped ... - risposta interrotta ... + + Legacy prompt template needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + Il modello di prompt precedente deve essere <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">aggiornato</a> nei Settaggi. - - processing ... - elaborazione ... + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + Nessun <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">modello di chat</a> configurato. - - generating response ... - generazione risposta ... + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + Il <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">modello di chat</a> non può essere vuoto. - - generating questions ... - generarzione domande ... + + Legacy system prompt needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + Il prompt del sistema precedente deve essere <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">aggiornato</a> nei Settaggi. - - + Copy Copia - - Copy Message - Copia messaggio - - - - Disable markdown - Disabilita Markdown - - - - Enable markdown - Abilita Markdown - - - - Thumbs up - Mi piace - - - - Gives a thumbs up to the response - Dà un mi piace alla risposta - - - - Thumbs down - Non mi piace - - - - Opens thumbs down dialog - Apre la finestra di dialogo "Non mi piace" - - - - Suggested follow-ups - Approfondimenti suggeriti - - - + Erase and reset chat session Cancella e ripristina la sessione di chat - + Copy chat session to clipboard Copia la sessione di chat negli appunti - - Redo last chat response - Riesegui l'ultima risposta della chat - - - + Add media Aggiungi contenuti multimediali - + Adds media to the prompt Aggiunge contenuti multimediali al prompt - + Stop generating Interrompi la generazione - + Stop the current response generation Arresta la generazione della risposta corrente - + Attach Allegare - + Single File File singolo - + Reloads the model Ricarica il modello - + <h3>Encountered an error loading model:</h3><br><i>"%1"</i><br><br>Model loading failures can happen for a variety of reasons, but the most common causes include a bad file format, an incomplete or corrupted download, the wrong file type, not enough system RAM or an incompatible model type. Here are some suggestions for resolving the problem:<br><ul><li>Ensure the model file has a compatible format and type<li>Check the model file is complete in the download folder<li>You can find the download folder in the settings dialog<li>If you've sideloaded the model ensure the file is not corrupt by checking md5sum<li>Read more about what models are supported in our <a href="https://docs.gpt4all.io/">documentation</a> for the gui<li>Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help <h3>Si è verificato un errore durante il caricamento del modello:</h3><br><i>"%1"</i><br><br>Gli errori di caricamento del modello possono verificarsi per diversi motivi, ma le cause più comuni includono un formato di file non valido, un download incompleto o danneggiato, il tipo di file sbagliato, RAM di sistema insufficiente o un tipo di modello incompatibile. Ecco alcuni suggerimenti per risolvere il problema:<br><ul><li>Assicurati che il file del modello abbia un formato e un tipo compatibili<li>Verifica che il file del modello sia completo nella cartella di download<li>Puoi trovare la cartella di download nella finestra di dialogo dei settaggi<li>Se hai scaricato manualmente il modello, assicurati che il file non sia danneggiato controllando md5sum<li>Leggi ulteriori informazioni su quali modelli sono supportati nella nostra <a href="https://docs.gpt4all.io/ ">documentazione</a> per la GUI<li>Consulta il nostro <a href="https://discord.gg/4M2QFmTt2k">canale Discord</a> per assistenza - - + + + Erase conversation? + Cancellare la conversazione? + + + + Changing the model will erase the current conversation. + La modifica del modello cancellerà la conversazione corrente. + + + + Reload · %1 Ricarica · %1 - + Loading · %1 Caricamento · %1 - + Load · %1 (default) → Carica · %1 (predefinito) → - - restoring from text ... - ripristino dal testo ... - - - - retrieving localdocs: %1 ... - recupero documenti locali: %1 ... - - - - searching localdocs: %1 ... - ricerca in documenti locali: %1 ... - - - - %n Source(s) - - %n Fonte - %n Fonti - - - - + Send a message... Manda un messaggio... - + Load a model to continue... Carica un modello per continuare... - + Send messages/prompts to the model Invia messaggi/prompt al modello - + Cut Taglia - + Paste Incolla - + Select All Seleziona tutto - + Send message Invia messaggio - + Sends the message/prompt contained in textfield to the model Invia il messaggio/prompt contenuto nel campo di testo al modello @@ -1118,40 +1577,53 @@ modello per iniziare Seleziona una raccolta per renderla disponibile al modello in chat. + + ConfirmationDialog + + + OK + OK + + + + Cancel + Annulla + + Download - + Model "%1" is installed successfully. Il modello "%1" è stato installato correttamente. - + ERROR: $MODEL_NAME is empty. ERRORE: $MODEL_NAME è vuoto. - + ERROR: $API_KEY is empty. ERRORE: $API_KEY è vuoto. - + ERROR: $BASE_URL is invalid. ERRORE: $BASE_URL non è valido. - + ERROR: Model "%1 (%2)" is conflict. ERRORE: il modello "%1 (%2)" è in conflitto. - + Model "%1 (%2)" is installed successfully. Il modello "%1 (%2)" è stato installato correttamente. - + Model "%1" is removed. Il modello "%1" è stato rimosso. @@ -1318,52 +1790,52 @@ modello per iniziare Applicazione predefinita - + Display Mostra - + Show Sources Mostra le fonti - + Display the sources used for each response. Visualizza le fonti utilizzate per ciascuna risposta. - + Advanced Avanzate - + Warning: Advanced usage only. Avvertenza: solo per uso avanzato. - + Values too large may cause localdocs failure, extremely slow responses or failure to respond at all. Roughly speaking, the {N chars x N snippets} are added to the model's context window. More info <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">here</a>. Valori troppo grandi possono causare errori di LocalDocs, risposte estremamente lente o l'impossibilità di rispondere. In parole povere, {N caratteri x N frammenti} vengono aggiunti alla finestra di contesto del modello. Maggiori informazioni <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">qui</a>. - + Document snippet size (characters) Dimensioni del frammento di documento (caratteri) - + Number of characters per document snippet. Larger numbers increase likelihood of factual responses, but also result in slower generation. Numero di caratteri per frammento di documento. Numeri più grandi aumentano la probabilità di risposte basate sui fatti, ma comportano anche una generazione più lenta. - + Max document snippets per prompt Numero massimo di frammenti di documento per prompt - + Max best N matches of retrieved document snippets to add to the context for prompt. Larger numbers increase likelihood of factual responses, but also result in slower generation. Il numero massimo di frammenti di documento recuperati che presentano le migliori corrispondenze, da includere nel contesto del prompt. Numeri più alti aumentano la probabilità di ricevere risposte basate sui fatti, ma comportano anche una generazione più lenta. @@ -1525,78 +1997,78 @@ modello per iniziare ModelList - + <ul><li>Requires personal OpenAI API key.</li><li>WARNING: Will send your chats to OpenAI!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with OpenAI</li><li>You can apply for an API key <a href="https://platform.openai.com/account/api-keys">here.</a></li> <ul><li>Richiede una chiave API OpenAI personale.</li><li>ATTENZIONE: invierà le tue chat a OpenAI!</li><li>La tua chiave API verrà archiviata su disco</li><li> Verrà utilizzato solo per comunicare con OpenAI</li><li>Puoi richiedere una chiave API <a href="https://platform.openai.com/account/api-keys">qui.</a> </li> - + <strong>OpenAI's ChatGPT model GPT-3.5 Turbo</strong><br> %1 - + <strong>OpenAI's ChatGPT model GPT-4</strong><br> %1 %2 - + <strong>Mistral Tiny model</strong><br> %1 - + <strong>Mistral Small model</strong><br> %1 - + <strong>Mistral Medium model</strong><br> %1 - + <br><br><i>* Even if you pay OpenAI for ChatGPT-4 this does not guarantee API key access. Contact OpenAI for more info. <br><br><i>* Anche se paghi OpenAI per ChatGPT-4 questo non garantisce l'accesso alla chiave API. Contatta OpenAI per maggiori informazioni. - - + + cannot open "%1": %2 impossibile aprire "%1": %2 - + cannot create "%1": %2 impossibile creare "%1": %2 - + %1 (%2) %1 (%2) - + <strong>OpenAI-Compatible API Model</strong><br><ul><li>API Key: %1</li><li>Base URL: %2</li><li>Model Name: %3</li></ul> <strong>Modello API compatibile con OpenAI</strong><br><ul><li>Chiave API: %1</li><li>URL di base: %2</li><li>Nome modello: %3</li></ul> - + <ul><li>Requires personal Mistral API key.</li><li>WARNING: Will send your chats to Mistral!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with Mistral</li><li>You can apply for an API key <a href="https://console.mistral.ai/user/api-keys">here</a>.</li> <ul><li>Richiede una chiave API Mistral personale.</li><li>ATTENZIONE: invierà le tue chat a Mistral!</li><li>La tua chiave API verrà archiviata su disco</li><li> Verrà utilizzato solo per comunicare con Mistral</li><li>Puoi richiedere una chiave API <a href="https://console.mistral.ai/user/api-keys">qui</a>. </li> - + <ul><li>Requires personal API key and the API base URL.</li><li>WARNING: Will send your chats to the OpenAI-compatible API Server you specified!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with the OpenAI-compatible API Server</li> <ul><li>Richiede una chiave API personale e l'URL di base dell'API.</li><li>ATTENZIONE: invierà le tue chat al server API compatibile con OpenAI che hai specificato!</li><li>La tua chiave API verrà archiviata su disco</li><li>Verrà utilizzata solo per comunicare con il server API compatibile con OpenAI</li> - + <strong>Connect to OpenAI-compatible API server</strong><br> %1 <strong>Connetti al server API compatibile con OpenAI</strong><br> %1 - + <strong>Created by %1.</strong><br><ul><li>Published on %2.<li>This model has %3 likes.<li>This model has %4 downloads.<li>More info can be found <a href="https://huggingface.co/%5">here.</a></ul> <strong>Creato da %1.</strong><br><ul><li>Pubblicato il %2.<li>Questo modello ha %3 Mi piace.<li>Questo modello ha %4 download.<li>Altro informazioni possono essere trovate <a href="https://huggingface.co/%5">qui.</a></ul> @@ -1609,87 +2081,155 @@ modello per iniziare Modello - + + %1 system message? + %1 il messaggio di sistema? + + + + + Clear + Cancella + + + + + Reset + Ripristina + + + + The system message will be %1. + Il messaggio di sistema verrà %1. + + + + removed + rimosso + + + + + reset to the default + ripristinato il valore predefinito + + + + %1 chat template? + %1 il modello di chat? + + + + The chat template will be %1. + Il modello di chat verrà %1. + + + + erased + cancellato + + + Model Settings Settaggi modello - + Clone Clona - + Remove Rimuovi - + Name Nome - + Model File File del modello - - System Prompt - Prompt di sistema + + System Message + Messaggio di sistema + + + + A message to set the context or guide the behavior of the model. Leave blank for none. NOTE: Since GPT4All 3.5, this should not contain control tokens. + Un messaggio per impostare il contesto o guidare il comportamento del modello. Lasciare vuoto per nessuno. NOTA: da GPT4All 3.5, questo non dovrebbe contenere token di controllo. + + + + System message is not <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">plain text</a>. + Il messaggio di sistema non è <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">testo normale</a>. + + + + Chat Template + Modello di chat + + + + This Jinja template turns the chat into input for the model. + Questo modello Jinja trasforma la chat in input per il modello. - - Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens. - Prefisso all'inizio di ogni conversazione. Deve contenere i token di inquadramento appropriati. + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + Nessun <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">modello di chat</a> configurato. - - Prompt Template - Schema del prompt + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + Il <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">modello di chat</a> non può essere vuoto. - - The template that wraps every prompt. - Lo schema che incorpora ogni prompt. + + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Syntax error</a>: %1 + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Errore di sintassi</a>: %1 - - Must contain the string "%1" to be replaced with the user's input. - Deve contenere la stringa "%1" da sostituire con l'input dell'utente. + + Chat template is not in <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Jinja format</a>. + Il modello di chat non è in <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">formato Jinja</a>. - + Chat Name Prompt Prompt del nome della chat - + Prompt used to automatically generate chat names. Prompt utilizzato per generare automaticamente nomi di chat. - + Suggested FollowUp Prompt Prompt di approfondimento suggerito - + Prompt used to generate suggested follow-up questions. Prompt utilizzato per generare le domande di approfondimento suggerite. - + Context Length Lunghezza del contesto - + Number of input and output tokens the model sees. Numero di token di input e output visualizzati dal modello. - + Maximum combined prompt/response tokens before information is lost. Using more context than the model was trained on will yield poor results. NOTE: Does not take effect until you reload the model. @@ -1698,128 +2238,128 @@ L'utilizzo di un contesto maggiore rispetto a quello su cui è stato addest NOTA: non ha effetto finché non si ricarica il modello. - + Temperature Temperatura - + Randomness of model output. Higher -> more variation. Casualità dell'uscita del modello. Più alto -> più variazione. - + Temperature increases the chances of choosing less likely tokens. NOTE: Higher temperature gives more creative but less predictable outputs. La temperatura aumenta le possibilità di scegliere token meno probabili. NOTA: una temperatura più elevata offre risultati più creativi ma meno prevedibili. - + Top-P - + Nucleus Sampling factor. Lower -> more predictable. Fattore di campionamento del nucleo. Inferiore -> più prevedibile. - + Only the most likely tokens up to a total probability of top_p can be chosen. NOTE: Prevents choosing highly unlikely tokens. Solo i token più probabili, fino a un totale di probabilità di Top-P, possono essere scelti. NOTA: impedisce la scelta di token altamente improbabili. - + Min-P - + Minimum token probability. Higher -> more predictable. Probabilità minima del token. Più alto -> più prevedibile. - + Sets the minimum relative probability for a token to be considered. Imposta la probabilità relativa minima affinché un token venga considerato. - + Top-K - + Size of selection pool for tokens. Dimensione del lotto di selezione per i token. - + Only the top K most likely tokens will be chosen from. Saranno scelti solo i primi K token più probabili. - + Max Length Lunghezza massima - + Maximum response length, in tokens. Lunghezza massima della risposta, in token. - + Prompt Batch Size Dimensioni del lotto di prompt - + The batch size used for prompt processing. La dimensione del lotto usata per l'elaborazione dei prompt. - + Amount of prompt tokens to process at once. NOTE: Higher values can speed up reading prompts but will use more RAM. Numero di token del prompt da elaborare contemporaneamente. NOTA: valori più alti possono velocizzare la lettura dei prompt ma utilizzeranno più RAM. - + Repeat Penalty Penalità di ripetizione - + Repetition penalty factor. Set to 1 to disable. Fattore di penalità di ripetizione. Impostare su 1 per disabilitare. - + Repeat Penalty Tokens Token di penalità ripetizione - + Number of previous tokens used for penalty. Numero di token precedenti utilizzati per la penalità. - + GPU Layers Livelli GPU - + Number of model layers to load into VRAM. Numero di livelli del modello da caricare nella VRAM. - + How many model layers to load into VRAM. Decrease this if GPT4All runs out of VRAM while loading this model. Lower values increase CPU load and RAM usage, and make inference slower. NOTE: Does not take effect until you reload the model. @@ -2076,21 +2616,37 @@ NOTA: non ha effetto finché non si ricarica il modello. - MySettingsStack + MySettingsLabel - Please choose a directory - Scegli una cartella + + Clear + Cancella + + + + Reset + Ripristina MySettingsTab - + + Restore defaults? + Ripristinare le impostazioni predefinite? + + + + This page of settings will be reset to the defaults. + Questa pagina di impostazioni verrà ripristinata ai valori predefiniti. + + + Restore Defaults - Riprista i valori predefiniti + Ripristina i valori predefiniti - + Restores settings dialog to a default state Ripristina la finestra di dialogo dei settaggi a uno stato predefinito @@ -2287,6 +2843,11 @@ NOTA: attivando questa funzione, invierai i tuoi dati al Datalake Open Source di Describes what will happen when you opt-in Descrive cosa accadrà quando effettuerai l'adesione + + + Opt-in to anonymous usage analytics used to improve GPT4All + Acconsenti all'analisi anonima dell'uso per migliorare GPT4All + @@ -2302,7 +2863,7 @@ NOTA: attivando questa funzione, invierai i tuoi dati al Datalake Open Source di Allow opt-in for anonymous usage statistics - Consenti l'attivazione di statistiche di utilizzo anonime + Consenti l'attivazione delle statistiche di utilizzo anonime @@ -2320,6 +2881,11 @@ NOTA: attivando questa funzione, invierai i tuoi dati al Datalake Open Source di Allow opt-out for anonymous usage statistics Consenti la disattivazione per le statistiche di utilizzo anonime + + + Opt-in to anonymous sharing of chats to the GPT4All Datalake + Acconsenti alla condivisione anonima delle chat con il GPT4All Datalake + @@ -2347,30 +2913,6 @@ NOTA: attivando questa funzione, invierai i tuoi dati al Datalake Open Source di Consenti la non adesione alla condivisione anonima delle chat nel GPT4All Datalake - - SwitchModelDialog - - - <b>Warning:</b> changing the model will erase the current conversation. Do you wish to continue? - <b>Avviso:</b> la modifica del modello cancellerà la conversazione corrente. Vuoi continuare? - - - - Continue - Continua - - - - Continue with model loading - Continuare con il caricamento del modello - - - - - Cancel - Annulla - - ThumbsDownDialog @@ -2407,125 +2949,135 @@ NOTA: attivando questa funzione, invierai i tuoi dati al Datalake Open Source di main - + <h3>Encountered an error starting up:</h3><br><i>"Incompatible hardware detected."</i><br><br>Unfortunately, your CPU does not meet the minimal requirements to run this program. In particular, it does not support AVX intrinsics which this program requires to successfully run a modern large language model. The only solution at this time is to upgrade your hardware to a more modern CPU.<br><br>See here for more information: <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">https://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> <h3>Si è verificato un errore all'avvio:</h3><br><i>"Rilevato hardware incompatibile."</i><br><br>Sfortunatamente, la tua CPU non soddisfa i requisiti minimi per eseguire questo programma. In particolare, non supporta gli elementi intrinseci AVX richiesti da questo programma per eseguire con successo un modello linguistico moderno e di grandi dimensioni. L'unica soluzione in questo momento è aggiornare il tuo hardware con una CPU più moderna.<br><br>Vedi qui per ulteriori informazioni: <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">https ://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> - + GPT4All v%1 - + + Restore + Ripristina + + + + Quit + Esci + + + <h3>Encountered an error starting up:</h3><br><i>"Inability to access settings file."</i><br><br>Unfortunately, something is preventing the program from accessing the settings file. This could be caused by incorrect permissions in the local app config directory where the settings file is located. Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help. <h3>Si è verificato un errore all'avvio:</h3><br><i>"Impossibile accedere al file dei settaggi."</i><br><br>Sfortunatamente, qualcosa impedisce al programma di accedere al file dei settaggi. Ciò potrebbe essere causato da autorizzazioni errate nella cartella di configurazione locale dell'app in cui si trova il file dei settaggi. Dai un'occhiata al nostro <a href="https://discord.gg/4M2QFmTt2k">canale Discord</a> per ricevere assistenza. - + Connection to datalake failed. La connessione al Datalake non è riuscita. - + Saving chats. Salvataggio delle chat. - + Network dialog Dialogo di rete - + opt-in to share feedback/conversations aderisci per condividere feedback/conversazioni - + Home view Vista iniziale - + Home view of application Vista iniziale dell'applicazione - + Home Inizia - + Chat view Vista chat - + Chat view to interact with models Vista chat per interagire con i modelli - + Chats Chat - - + + Models Modelli - + Models view for installed models Vista modelli per i modelli installati - - + + LocalDocs - + LocalDocs view to configure and use local docs Vista LocalDocs per configurare e utilizzare i documenti locali - - + + Settings Settaggi - + Settings view for application configuration Vista dei settaggi per la configurazione dell'applicazione - + The datalake is enabled Il Datalake è abilitato - + Using a network model Utilizzando un modello di rete - + Server mode is enabled La modalità server è abilitata - + Installed models Modelli installati - + View of installed models Vista dei modelli installati diff --git a/gpt4all-chat/translations/gpt4all_pt_BR.ts b/gpt4all-chat/translations/gpt4all_pt_BR.ts index b82a025e4868..9e3fd1c97ee7 100644 --- a/gpt4all-chat/translations/gpt4all_pt_BR.ts +++ b/gpt4all-chat/translations/gpt4all_pt_BR.ts @@ -63,6 +63,467 @@ Criar Coleção + + AddGPT4AllModelView + + + These models have been specifically configured for use in GPT4All. The first few models on the list are known to work the best, but you should only attempt to use models that will fit in your available memory. + + + + + Network error: could not retrieve %1 + Erro de rede: não foi possível obter %1 + + + + + Busy indicator + + + + + Displayed when the models request is ongoing + xibido enquanto os modelos estão sendo carregados + + + + Model file + Arquivo do modelo + + + + Model file to be downloaded + Arquivo do modelo a ser baixado + + + + Description + Descrição + + + + File description + Descrição do arquivo + + + + Cancel + Cancelar + + + + Resume + Retomar + + + + Download + Baixar + + + + Stop/restart/start the download + Parar/reiniciar/iniciar o download + + + + Remove + Remover + + + + Remove model from filesystem + + + + + + Install + Instalar + + + + Install online model + Instalar modelo online + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + <strong><font size="1"><a href="#error">Erro</a></strong></font> + + + + Describes an error that occurred when downloading + + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + + + + + Error for incompatible hardware + + + + + Download progressBar + + + + + Shows the progress made in the download + Mostra o progresso do download + + + + Download speed + Velocidade de download + + + + Download speed in bytes/kilobytes/megabytes per second + Velocidade de download em bytes/kilobytes/megabytes por segundo + + + + Calculating... + Calculando... + + + + + + + Whether the file hash is being calculated + + + + + Displayed when the file hash is being calculated + + + + + ERROR: $API_KEY is empty. + + + + + enter $API_KEY + inserir $API_KEY + + + + ERROR: $BASE_URL is empty. + ERRO: A $BASE_URL está vazia. + + + + enter $BASE_URL + inserir a $BASE_URL + + + + ERROR: $MODEL_NAME is empty. + + + + + enter $MODEL_NAME + inserir o $MODEL_NAME + + + + File size + Tamanho do arquivo + + + + RAM required + RAM necessária + + + + %1 GB + %1 GB + + + + + ? + ? + + + + Parameters + Parâmetros + + + + Quant + Quant + + + + Type + Tipo + + + + AddHFModelView + + + Use the search to find and download models from HuggingFace. There is NO GUARANTEE that these will work. Many will require additional configuration before they can be used. + + + + + Discover and download models by keyword search... + Pesquisar modelos... + + + + Text field for discovering and filtering downloadable models + Campo de texto para descobrir e filtrar modelos para download + + + + Searching · %1 + Pesquisando · %1 + + + + Initiate model discovery and filtering + Pesquisar e filtrar modelos + + + + Triggers discovery and filtering of models + Aciona a descoberta e filtragem de modelos + + + + Default + Padrão + + + + Likes + Curtidas + + + + Downloads + Downloads + + + + Recent + Recentes + + + + Sort by: %1 + Ordenar por: %1 + + + + Asc + Asc + + + + Desc + Desc + + + + Sort dir: %1 + Ordenar diretório: %1 + + + + None + Nenhum + + + + Limit: %1 + Limite: %1 + + + + Model file + Arquivo do modelo + + + + Model file to be downloaded + Arquivo do modelo a ser baixado + + + + Description + Descrição + + + + File description + Descrição do arquivo + + + + Cancel + Cancelar + + + + Resume + Retomar + + + + Download + Baixar + + + + Stop/restart/start the download + Parar/reiniciar/iniciar o download + + + + Remove + Remover + + + + Remove model from filesystem + + + + + + Install + Instalar + + + + Install online model + Instalar modelo online + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + <strong><font size="1"><a href="#error">Erro</a></strong></font> + + + + Describes an error that occurred when downloading + + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + + + + + Error for incompatible hardware + + + + + Download progressBar + + + + + Shows the progress made in the download + Mostra o progresso do download + + + + Download speed + Velocidade de download + + + + Download speed in bytes/kilobytes/megabytes per second + Velocidade de download em bytes/kilobytes/megabytes por segundo + + + + Calculating... + Calculando... + + + + + + + Whether the file hash is being calculated + + + + + Busy indicator + + + + + Displayed when the file hash is being calculated + + + + + ERROR: $API_KEY is empty. + + + + + enter $API_KEY + inserir $API_KEY + + + + ERROR: $BASE_URL is empty. + ERRO: A $BASE_URL está vazia. + + + + enter $BASE_URL + inserir a $BASE_URL + + + + ERROR: $MODEL_NAME is empty. + + + + + enter $MODEL_NAME + inserir o $MODEL_NAME + + + + File size + Tamanho do arquivo + + + + Quant + Quant + + + + Type + Tipo + + AddModelView @@ -76,280 +537,230 @@ Descobrir Modelos - + + GPT4All + GPT4All + + + + HuggingFace + + + Discover and download models by keyword search... - Pesquisar modelos... + Pesquisar modelos... - Text field for discovering and filtering downloadable models - Campo de texto para descobrir e filtrar modelos para download + Campo de texto para descobrir e filtrar modelos para download - Initiate model discovery and filtering - Pesquisar e filtrar modelos + Pesquisar e filtrar modelos - Triggers discovery and filtering of models - Aciona a descoberta e filtragem de modelos + Aciona a descoberta e filtragem de modelos - Default - Padrão + Padrão - Likes - Curtidas + Curtidas - Downloads - Downloads + Downloads - Recent - Recentes + Recentes - Asc - Asc + Asc - Desc - Desc + Desc - None - Nenhum + Nenhum - Searching · %1 - Pesquisando · %1 + Pesquisando · %1 - Sort by: %1 - Ordenar por: %1 + Ordenar por: %1 - Sort dir: %1 - Ordenar diretório: %1 + Ordenar diretório: %1 - Limit: %1 - Limite: %1 + Limite: %1 - Network error: could not retrieve %1 - Erro de rede: não foi possível obter %1 + Erro de rede: não foi possível obter %1 - - Busy indicator - Indicador de processamento + Indicador de processamento - Displayed when the models request is ongoing - xibido enquanto os modelos estão sendo carregados + xibido enquanto os modelos estão sendo carregados - Model file - Arquivo do modelo + Arquivo do modelo - Model file to be downloaded - Arquivo do modelo a ser baixado + Arquivo do modelo a ser baixado - Description - Descrição + Descrição - File description - Descrição do arquivo + Descrição do arquivo - Cancel - Cancelar + Cancelar - Resume - Retomar + Retomar - Download - Baixar + Baixar - Stop/restart/start the download - Parar/reiniciar/iniciar o download + Parar/reiniciar/iniciar o download - Remove - Remover + Remover - Remove model from filesystem - Remover modelo do sistema + Remover modelo do sistema - - Install - Instalar + Instalar - Install online model - Instalar modelo online + Instalar modelo online - <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> - <strong><font size="2">ATENÇÃO: Este modelo não é recomendado para seu hardware. Ele exige mais memória (%1 GB) do que seu sistema possui (%2).</strong></font> + <strong><font size="2">ATENÇÃO: Este modelo não é recomendado para seu hardware. Ele exige mais memória (%1 GB) do que seu sistema possui (%2).</strong></font> - ERROR: $API_KEY is empty. - ERRO: A $API_KEY está vazia. + ERRO: A $API_KEY está vazia. - ERROR: $BASE_URL is empty. - ERRO: A $BASE_URL está vazia. + ERRO: A $BASE_URL está vazia. - enter $BASE_URL - inserir a $BASE_URL + inserir a $BASE_URL - ERROR: $MODEL_NAME is empty. - ERRO: O $MODEL_NAME está vazio. + ERRO: O $MODEL_NAME está vazio. - enter $MODEL_NAME - inserir o $MODEL_NAME + inserir o $MODEL_NAME - %1 GB - %1 GB + %1 GB - - ? - ? + ? - Describes an error that occurred when downloading - Mostra informações sobre o erro no download + Mostra informações sobre o erro no download - <strong><font size="1"><a href="#error">Error</a></strong></font> - <strong><font size="1"><a href="#error">Erro</a></strong></font> + <strong><font size="1"><a href="#error">Erro</a></strong></font> - Error for incompatible hardware - Aviso: Hardware não compatível + Aviso: Hardware não compatível - Download progressBar - Progresso do download + Progresso do download - Shows the progress made in the download - Mostra o progresso do download + Mostra o progresso do download - Download speed - Velocidade de download + Velocidade de download - Download speed in bytes/kilobytes/megabytes per second - Velocidade de download em bytes/kilobytes/megabytes por segundo + Velocidade de download em bytes/kilobytes/megabytes por segundo - Calculating... - Calculando... + Calculando... - - - - Whether the file hash is being calculated - Quando o hash do arquivo está sendo calculado + Quando o hash do arquivo está sendo calculado - Displayed when the file hash is being calculated - Exibido durante o cálculo do hash do arquivo + Exibido durante o cálculo do hash do arquivo - enter $API_KEY - inserir $API_KEY + inserir $API_KEY - File size - Tamanho do arquivo + Tamanho do arquivo - RAM required - RAM necessária + RAM necessária - Parameters - Parâmetros + Parâmetros - Quant - Quant + Quant - Type - Tipo + Tipo @@ -553,14 +964,22 @@ + Enable System Tray + + + + + The application will minimize to the system tray when the window is closed. + + + Save Chat Context I used "Histórico do Chat" (Chat History) instead of "Contexto do Chat" (Chat Context) to clearly convey that it refers to saving past messages, making it more intuitive and avoiding potential confusion with abstract terms. - Salvar Histórico do Chat + Salvar Histórico do Chat - Save the chat model's state to disk for faster loading. WARNING: Uses ~2GB per chat. - Salvar histórico do chat para carregamento mais rápido. (Usa aprox. 2GB por chat). + Salvar histórico do chat para carregamento mais rápido. (Usa aprox. 2GB por chat). @@ -601,13 +1020,13 @@ Chat - - + + New Chat Novo Chat - + Server Chat Chat com o Servidor @@ -615,12 +1034,12 @@ ChatAPIWorker - + ERROR: Network error occurred while connecting to the API server ERRO: Ocorreu um erro de rede ao conectar-se ao servidor da API - + ChatAPIWorker::handleFinished got HTTP Error %1 %2 ChatAPIWorker::handleFinished recebeu erro HTTP %1 %2 @@ -688,35 +1107,181 @@ Lista de chats na caixa de diálogo do menu lateral + + ChatItemView + + + GPT4All + GPT4All + + + + You + Você + + + + response stopped ... + resposta interrompida... + + + + retrieving localdocs: %1 ... + Recuperando dados em LocalDocs: %1 ... + + + + searching localdocs: %1 ... + Buscando em LocalDocs: %1 ... + + + + processing ... + processando... + + + + generating response ... + gerando resposta... + + + + generating questions ... + gerando perguntas... + + + + + Copy + Copiar + + + + Copy Message + Copiar Mensagem + + + + Disable markdown + Desativar markdown + + + + Enable markdown + Ativar markdown + + + + %n Source(s) + + %n Origem + %n Origens + + + + + LocalDocs + LocalDocs + + + + Edit this message? + + + + + + All following messages will be permanently erased. + + + + + Redo this response? + + + + + Cannot edit chat without a loaded model. + + + + + Cannot edit chat while the model is generating. + + + + + Edit + + + + + Cannot redo response without a loaded model. + + + + + Cannot redo response while the model is generating. + + + + + Redo + + + + + Like response + + + + + Dislike response + + + + + Suggested follow-ups + Perguntas relacionadas + + + + ChatLLM + + + Your message was too long and could not be processed (%1 > %2). Please try again with something shorter. + + + ChatListModel - + TODAY HOJE - + THIS WEEK ESTA SEMANA - + THIS MONTH ESTE MÊS - + LAST SIX MONTHS ÚLTIMOS SEIS MESES - + THIS YEAR ESTE ANO - + LAST YEAR ANO PASSADO @@ -724,350 +1289,363 @@ ChatView - + <h3>Warning</h3><p>%1</p> <h3>Aviso</h3><p>%1</p> - Switch model dialog - Mensagem ao troca de modelo + Mensagem ao troca de modelo - Warn the user if they switch models, then context will be erased - Ao trocar de modelo, o contexto da conversa será apagado + Ao trocar de modelo, o contexto da conversa será apagado - + Conversation copied to clipboard. Conversa copiada. - + Code copied to clipboard. Código copiado. - + + The entire chat will be erased. + + + + Chat panel Painel de chat - + Chat panel with options Painel de chat com opções - + Reload the currently loaded model Recarregar modelo atual - + Eject the currently loaded model Ejetar o modelo carregado atualmente - + No model installed. Nenhum modelo instalado. - + Model loading error. Erro ao carregar o modelo. - + Waiting for model... Aguardando modelo... - + Switching context... Mudando de contexto... - + Choose a model... Escolha um modelo... - + Not found: %1 Não encontrado: %1 - + The top item is the current model O modelo atual é exibido no topo - - + LocalDocs LocalDocs - + Add documents Adicionar documentos - + add collections of documents to the chat Adicionar Coleção de Documentos - + Load the default model Carregar o modelo padrão - + Loads the default model which can be changed in settings Carrega o modelo padrão (personalizável nas configurações) - + No Model Installed Nenhum Modelo Instalado - + GPT4All requires that you install at least one model to get started O GPT4All precisa de pelo menos um modelo modelo instalado para funcionar - + Install a Model Instalar um Modelo - + Shows the add model view Mostra a visualização para adicionar modelo - + Conversation with the model Conversa com o modelo - + prompt / response pairs from the conversation Pares de pergunta/resposta da conversa - + + Legacy prompt template needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + + + + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + + + + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + + + + + Legacy system prompt needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + + + GPT4All - GPT4All + GPT4All - You - Você + Você - response stopped ... - resposta interrompida... + resposta interrompida... - processing ... - processando... + processando... - generating response ... - gerando resposta... + gerando resposta... - generating questions ... - gerando perguntas... + gerando perguntas... - - + Copy Copiar - Copy Message - Copiar Mensagem + Copiar Mensagem - Disable markdown - Desativar markdown + Desativar markdown - Enable markdown - Ativar markdown + Ativar markdown - Thumbs up - Resposta boa + Resposta boa - Gives a thumbs up to the response - Curte a resposta + Curte a resposta - Thumbs down - Resposta ruim + Resposta ruim - Opens thumbs down dialog - Abrir diálogo de joinha para baixo + Abrir diálogo de joinha para baixo - Suggested follow-ups - Perguntas relacionadas + Perguntas relacionadas - + Erase and reset chat session Apagar e redefinir sessão de chat - + Copy chat session to clipboard Copiar histórico da conversa - Redo last chat response - Refazer última resposta + Refazer última resposta - + Add media Adicionar mídia - + Adds media to the prompt Adiciona mídia ao prompt - + Stop generating Parar de gerar - + Stop the current response generation Parar a geração da resposta atual - + Attach Anexar - + Single File Arquivo Único - + Reloads the model Recarrega modelo - + <h3>Encountered an error loading model:</h3><br><i>"%1"</i><br><br>Model loading failures can happen for a variety of reasons, but the most common causes include a bad file format, an incomplete or corrupted download, the wrong file type, not enough system RAM or an incompatible model type. Here are some suggestions for resolving the problem:<br><ul><li>Ensure the model file has a compatible format and type<li>Check the model file is complete in the download folder<li>You can find the download folder in the settings dialog<li>If you've sideloaded the model ensure the file is not corrupt by checking md5sum<li>Read more about what models are supported in our <a href="https://docs.gpt4all.io/">documentation</a> for the gui<li>Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help <h3>Ocorreu um erro ao carregar o modelo:</h3><br><i>"%1"</i><br><br>Falhas no carregamento do modelo podem acontecer por vários motivos, mas as causas mais comuns incluem um formato de arquivo incorreto, um download incompleto ou corrompido, o tipo de arquivo errado, memória RAM do sistema insuficiente ou um tipo de modelo incompatível. Aqui estão algumas sugestões para resolver o problema:<br><ul><li>Certifique-se de que o arquivo do modelo tenha um formato e tipo compatíveis<li>Verifique se o arquivo do modelo está completo na pasta de download<li>Você pode encontrar a pasta de download na caixa de diálogo de configurações<li>Se você carregou o modelo, certifique-se de que o arquivo não esteja corrompido verificando o md5sum<li>Leia mais sobre quais modelos são suportados em nossa <a href="https://docs.gpt4all.io/">documentação</a> para a interface gráfica<li>Confira nosso <a href="https://discord.gg/4M2QFmTt2k">canal do Discord</a> para obter ajuda - - + + + Erase conversation? + + + + + Changing the model will erase the current conversation. + + + + + Reload · %1 Recarregar · %1 - + Loading · %1 Carregando · %1 - + Load · %1 (default) → Carregar · %1 (padrão) → - restoring from text ... - Recuperando do texto... + Recuperando do texto... - retrieving localdocs: %1 ... - Recuperando dados em LocalDocs: %1 ... + Recuperando dados em LocalDocs: %1 ... - searching localdocs: %1 ... - Buscando em LocalDocs: %1 ... + Buscando em LocalDocs: %1 ... - %n Source(s) - + %n Origem %n Origens - + Send a message... Enviar uma mensagem... - + Load a model to continue... Carregue um modelo para continuar... - + Send messages/prompts to the model Enviar mensagens/prompts para o modelo - + Cut Recortar - + Paste Colar - + Select All Selecionar tudo - + Send message Enviar mensagem - + Sends the message/prompt contained in textfield to the model Envia a mensagem/prompt contida no campo de texto para o modelo @@ -1111,40 +1689,53 @@ modelo instalado para funcionar Selecione uma coleção para disponibilizá-la ao modelo de chat. + + ConfirmationDialog + + + OK + + + + + Cancel + Cancelar + + Download - + Model "%1" is installed successfully. Modelo "%1" instalado com sucesso. - + ERROR: $MODEL_NAME is empty. ERRO: O nome do modelo ($MODEL_NAME) está vazio. - + ERROR: $API_KEY is empty. ERRO: A chave da API ($API_KEY) está vazia. - + ERROR: $BASE_URL is invalid. ERRO: A URL base ($BASE_URL) é inválida. - + ERROR: Model "%1 (%2)" is conflict. ERRO: Conflito com o modelo "%1 (%2)". - + Model "%1 (%2)" is installed successfully. Modelo "%1 (%2)" instalado com sucesso. - + Model "%1" is removed. Modelo "%1" removido. @@ -1310,53 +1901,53 @@ modelo instalado para funcionar Aplicativo padrão - + Display Exibir - + Show Sources Mostrar Fontes - + Display the sources used for each response. Mostra as fontes usadas para cada resposta. - + Advanced Apenas para usuários avançados - + Warning: Advanced usage only. Atenção: Apenas para usuários avançados. - + Values too large may cause localdocs failure, extremely slow responses or failure to respond at all. Roughly speaking, the {N chars x N snippets} are added to the model's context window. More info <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">here</a>. Valores muito altos podem causar falhas no LocalDocs, respostas extremamente lentas ou até mesmo nenhuma resposta. De forma geral, o valor {Número de Caracteres x Número de Trechos} é adicionado à janela de contexto do modelo. Clique <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">aqui</a> para mais informações. - + Document snippet size (characters) I translated "snippet" as "trecho" to make the term feel more natural and understandable in Portuguese. "Trecho" effectively conveys the idea of a portion or section of a document, fitting well within the context, whereas a more literal translation might sound less intuitive or awkward for users. Tamanho do trecho de documento (caracteres) - + Number of characters per document snippet. Larger numbers increase likelihood of factual responses, but also result in slower generation. Número de caracteres por trecho de documento. Valores maiores aumentam a chance de respostas factuais, mas também tornam a geração mais lenta. - + Max document snippets per prompt Máximo de Trechos de Documento por Prompt - + Max best N matches of retrieved document snippets to add to the context for prompt. Larger numbers increase likelihood of factual responses, but also result in slower generation. Número máximo de trechos de documentos a serem adicionados ao contexto do prompt. Valores maiores aumentam a chance de respostas factuais, mas também tornam a geração mais lenta. @@ -1518,78 +2109,78 @@ modelo instalado para funcionar ModelList - + <ul><li>Requires personal OpenAI API key.</li><li>WARNING: Will send your chats to OpenAI!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with OpenAI</li><li>You can apply for an API key <a href="https://platform.openai.com/account/api-keys">here.</a></li> <ul><li>É necessária uma chave de API da OpenAI.</li><li>AVISO: Seus chats serão enviados para a OpenAI!</li><li>Sua chave de API será armazenada localmente</li><li>Ela será usada apenas para comunicação com a OpenAI</li><li>Você pode solicitar uma chave de API <a href="https://platform.openai.com/account/api-keys">aqui.</a></li> - + <strong>OpenAI's ChatGPT model GPT-3.5 Turbo</strong><br> %1 <strong>Modelo ChatGPT GPT-3.5 Turbo da OpenAI</strong><br> %1 - + <strong>OpenAI's ChatGPT model GPT-4</strong><br> %1 %2 <strong>Modelo ChatGPT GPT-4 da OpenAI</strong><br> %1 %2 - + <strong>Mistral Tiny model</strong><br> %1 <strong>Modelo Mistral Tiny</strong><br> %1 - + <strong>Mistral Small model</strong><br> %1 <strong>Modelo Mistral Small</strong><br> %1 - + <strong>Mistral Medium model</strong><br> %1 <strong>Modelo Mistral Medium</strong><br> %1 - + <br><br><i>* Even if you pay OpenAI for ChatGPT-4 this does not guarantee API key access. Contact OpenAI for more info. <br><br><i>* Mesmo que você pague pelo ChatGPT-4 da OpenAI, isso não garante acesso à chave de API. Contate a OpenAI para mais informações. - - + + cannot open "%1": %2 não é possível abrir "%1": %2 - + cannot create "%1": %2 não é possível criar "%1": %2 - + %1 (%2) %1 (%2) - + <strong>OpenAI-Compatible API Model</strong><br><ul><li>API Key: %1</li><li>Base URL: %2</li><li>Model Name: %3</li></ul> <strong>Modelo de API Compatível com OpenAI</strong><br><ul><li>Chave da API: %1</li><li>URL Base: %2</li><li>Nome do Modelo: %3</li></ul> - + <ul><li>Requires personal Mistral API key.</li><li>WARNING: Will send your chats to Mistral!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with Mistral</li><li>You can apply for an API key <a href="https://console.mistral.ai/user/api-keys">here</a>.</li> <ul><li>É necessária uma chave de API da Mistral.</li><li>AVISO: Seus chats serão enviados para a Mistral!</li><li>Sua chave de API será armazenada localmente</li><li>Ela será usada apenas para comunicação com a Mistral</li><li>Você pode solicitar uma chave de API <a href="https://console.mistral.ai/user/api-keys">aqui</a>.</li> - + <ul><li>Requires personal API key and the API base URL.</li><li>WARNING: Will send your chats to the OpenAI-compatible API Server you specified!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with the OpenAI-compatible API Server</li> <ul><li>É necessária uma chave de API e a URL da API.</li><li>AVISO: Seus chats serão enviados para o servidor de API compatível com OpenAI que você especificou!</li><li>Sua chave de API será armazenada no disco</li><li>Será usada apenas para comunicação com o servidor de API compatível com OpenAI</li> - + <strong>Connect to OpenAI-compatible API server</strong><br> %1 <strong>Conectar a um servidor de API compatível com OpenAI</strong><br> %1 - + <strong>Created by %1.</strong><br><ul><li>Published on %2.<li>This model has %3 likes.<li>This model has %4 downloads.<li>More info can be found <a href="https://huggingface.co/%5">here.</a></ul> <strong>Criado por %1.</strong><br><ul><li>Publicado em %2.<li>Este modelo tem %3 curtidas.<li>Este modelo tem %4 downloads.<li>Mais informações podem ser encontradas <a href="https://huggingface.co/%5">aqui.</a></ul> @@ -1602,87 +2193,175 @@ modelo instalado para funcionar Modelo - + + %1 system message? + + + + + + Clear + + + + + + Reset + + + + + The system message will be %1. + + + + + removed + + + + + + reset to the default + + + + + %1 chat template? + + + + + The chat template will be %1. + + + + + erased + + + + Model Settings Configurações do Modelo - + Clone Clonar - + Remove Remover - + Name Nome - + Model File Arquivo do Modelo - System Prompt - Prompt do Sistema + Prompt do Sistema - Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens. - Prefixado no início de cada conversa. Deve conter os tokens de enquadramento apropriados. + Prefixado no início de cada conversa. Deve conter os tokens de enquadramento apropriados. - Prompt Template - Modelo de Prompt + Modelo de Prompt - The template that wraps every prompt. - Modelo para cada prompt. + Modelo para cada prompt. - Must contain the string "%1" to be replaced with the user's input. - Deve incluir "%1" para a entrada do usuário. + Deve incluir "%1" para a entrada do usuário. + + + + System Message + + + + + A message to set the context or guide the behavior of the model. Leave blank for none. NOTE: Since GPT4All 3.5, this should not contain control tokens. + + + + + System message is not <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">plain text</a>. + + + + + Chat Template + + + + + This Jinja template turns the chat into input for the model. + - + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + + + + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + + + + + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Syntax error</a>: %1 + + + + + Chat template is not in <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Jinja format</a>. + + + + Chat Name Prompt Prompt para Nome do Chat - + Prompt used to automatically generate chat names. Prompt usado para gerar automaticamente nomes de chats. - + Suggested FollowUp Prompt Prompt de Sugestão de Acompanhamento - + Prompt used to generate suggested follow-up questions. Prompt usado para gerar sugestões de perguntas. - + Context Length Tamanho do Contexto - + Number of input and output tokens the model sees. Tamanho da Janela de Contexto. - + Maximum combined prompt/response tokens before information is lost. Using more context than the model was trained on will yield poor results. NOTE: Does not take effect until you reload the model. @@ -1691,128 +2370,128 @@ Usar mais contexto do que o modelo foi treinado pode gerar resultados ruins. Obs.: Só entrará em vigor após recarregar o modelo. - + Temperature Temperatura - + Randomness of model output. Higher -> more variation. Aleatoriedade das respostas. Quanto maior, mais variadas. - + Temperature increases the chances of choosing less likely tokens. NOTE: Higher temperature gives more creative but less predictable outputs. Aumenta a chance de escolher tokens menos prováveis. Obs.: Uma temperatura mais alta gera resultados mais criativos, mas menos previsíveis. - + Top-P Top-P - + Nucleus Sampling factor. Lower -> more predictable. Amostragem por núcleo. Menor valor, respostas mais previsíveis. - + Only the most likely tokens up to a total probability of top_p can be chosen. NOTE: Prevents choosing highly unlikely tokens. Apenas tokens com probabilidade total até o valor de top_p serão escolhidos. Obs.: Evita tokens muito improváveis. - + Min-P Min-P - + Minimum token probability. Higher -> more predictable. Probabilidade mínima do token. Quanto maior -> mais previsível. - + Sets the minimum relative probability for a token to be considered. Define a probabilidade relativa mínima para um token ser considerado. - + Top-K Top-K - + Size of selection pool for tokens. Número de tokens considerados na amostragem. - + Only the top K most likely tokens will be chosen from. Serão escolhidos apenas os K tokens mais prováveis. - + Max Length Comprimento Máximo - + Maximum response length, in tokens. Comprimento máximo da resposta, em tokens. - + Prompt Batch Size Tamanho do Lote de Processamento - + The batch size used for prompt processing. Tokens processados por lote. - + Amount of prompt tokens to process at once. NOTE: Higher values can speed up reading prompts but will use more RAM. Quantidade de tokens de prompt para processar de uma vez. OBS.: Valores mais altos podem acelerar a leitura dos prompts, mas usarão mais RAM. - + Repeat Penalty Penalidade de Repetição - + Repetition penalty factor. Set to 1 to disable. Penalidade de Repetição (1 para desativar). - + Repeat Penalty Tokens Tokens para penalizar repetição - + Number of previous tokens used for penalty. Número de tokens anteriores usados para penalidade. - + GPU Layers Camadas na GPU - + Number of model layers to load into VRAM. Camadas Carregadas na GPU. - + How many model layers to load into VRAM. Decrease this if GPT4All runs out of VRAM while loading this model. Lower values increase CPU load and RAM usage, and make inference slower. NOTE: Does not take effect until you reload the model. @@ -2068,6 +2747,19 @@ Obs.: Só entrará em vigor após recarregar o modelo. Escolha um diretório + + MySettingsLabel + + + Clear + + + + + Reset + + + MySettingsStack @@ -2078,12 +2770,22 @@ Obs.: Só entrará em vigor após recarregar o modelo. MySettingsTab - + + Restore defaults? + + + + + This page of settings will be reset to the defaults. + + + + Restore Defaults Restaurar Configurações Padrão - + Restores settings dialog to a default state Restaura as configurações para o estado padrão @@ -2290,6 +2992,11 @@ versão do modelo GPT4All que utilize seus dados! Describes what will happen when you opt-in Descrição do que acontece ao participar + + + Opt-in to anonymous usage analytics used to improve GPT4All + + @@ -2323,6 +3030,11 @@ versão do modelo GPT4All que utilize seus dados! Allow opt-out for anonymous usage statistics Permitir recusar envio de estatísticas de uso anônimas + + + Opt-in to anonymous sharing of chats to the GPT4All Datalake + + @@ -2353,25 +3065,20 @@ versão do modelo GPT4All que utilize seus dados! SwitchModelDialog - <b>Warning:</b> changing the model will erase the current conversation. Do you wish to continue? - <b>Atenção:</b> Ao trocar o modelo a conversa atual será perdida. Continuar? + <b>Atenção:</b> Ao trocar o modelo a conversa atual será perdida. Continuar? - Continue - Continuar + Continuar - Continue with model loading - Confirma a troca do modelo + Confirma a troca do modelo - - Cancel - Cancelar + Cancelar @@ -2410,125 +3117,135 @@ versão do modelo GPT4All que utilize seus dados! main - + <h3>Encountered an error starting up:</h3><br><i>"Incompatible hardware detected."</i><br><br>Unfortunately, your CPU does not meet the minimal requirements to run this program. In particular, it does not support AVX intrinsics which this program requires to successfully run a modern large language model. The only solution at this time is to upgrade your hardware to a more modern CPU.<br><br>See here for more information: <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">https://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> <h3>Ocorreu um erro ao iniciar:</h3><br><i>"Hardware incompatível detectado."</i><br><br>Infelizmente, seu processador não atende aos requisitos mínimos para executar este programa. Especificamente, ele não possui suporte às instruções AVX, que são necessárias para executar modelos de linguagem grandes e modernos. A única solução, no momento, é atualizar seu hardware para um processador mais recente.<br><br>Para mais informações, consulte: <a href="https://pt.wikipedia.org/wiki/Advanced_Vector_Extensions">https://pt.wikipedia.org/wiki/Advanced_Vector_Extensions</a> - + GPT4All v%1 GPT4All v%1 - + + Restore + + + + + Quit + + + + <h3>Encountered an error starting up:</h3><br><i>"Inability to access settings file."</i><br><br>Unfortunately, something is preventing the program from accessing the settings file. This could be caused by incorrect permissions in the local app config directory where the settings file is located. Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help. <h3>Ocorreu um erro ao iniciar:</h3><br><i>"Não foi possível acessar o arquivo de configurações."</i><br><br>Infelizmente, algo está impedindo o programa de acessar o arquivo de configurações. Isso pode acontecer devido a permissões incorretas na pasta de configurações do aplicativo. Para obter ajuda, acesse nosso <a href="https://discord.gg/4M2QFmTt2k">canal no Discord</a>. - + Connection to datalake failed. Falha na conexão com o datalake. - + Saving chats. Salvando chats. - + Network dialog Avisos de rede - + opt-in to share feedback/conversations permitir compartilhamento de feedback/conversas - + Home view Tela inicial - + Home view of application Tela inicial do aplicativo - + Home Início - + Chat view Visualização do Chat - + Chat view to interact with models Visualização do chat para interagir com os modelos - + Chats Chats - - + + Models Modelos - + Models view for installed models Tela de modelos instalados - - + + LocalDocs LocalDocs - + LocalDocs view to configure and use local docs Tela de configuração e uso de documentos locais do LocalDocs - - + + Settings Config - + Settings view for application configuration Tela de configurações do aplicativo - + The datalake is enabled O datalake está ativado - + Using a network model Usando um modelo de rede - + Server mode is enabled Modo servidor ativado - + Installed models Modelos instalados - + View of installed models Exibe os modelos instalados diff --git a/gpt4all-chat/translations/gpt4all_ro_RO.ts b/gpt4all-chat/translations/gpt4all_ro_RO.ts index 0c9227c99ade..199fa6aad1f7 100644 --- a/gpt4all-chat/translations/gpt4all_ro_RO.ts +++ b/gpt4all-chat/translations/gpt4all_ro_RO.ts @@ -64,292 +64,615 @@ - AddModelView + AddGPT4AllModelView - - ← Existing Models - ← Modelele curente/instalate + + These models have been specifically configured for use in GPT4All. The first few models on the list are known to work the best, but you should only attempt to use models that will fit in your available memory. + Aceste modele au fost configurate special pentru utilizarea în GPT4All. Primele câteva modele din listă sunt cunoscute ca fiind cele mai bune, dar ar trebui să încercați să utilizați doar modele care se încadrează în RAM. - - Explore Models - Caută modele + + Network error: could not retrieve %1 + Eroare de reţea: nu se poate prelua %1 + + + + + Busy indicator + Indicator de activitate + + + + Displayed when the models request is ongoing + Afişat în timpul solicitării modelului + + + + Model file + Fişierul modelului + + + + Model file to be downloaded + Fişierul modelului ce va fi descărcat + + + + Description + Descriere + + + + File description + Descrierea fişierului + + + + Cancel + Anulare + + + + Resume + Continuare + + + + Download + Download + + + + Stop/restart/start the download + Oprirea/Repornirea/Iniţierea descărcării + + + + Remove + Şterg + + + + Remove model from filesystem + Şterg modelul din sistemul de fişiere + + + + + Install + Instalare + + + + Install online model + Instalez un model din online + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + <strong><font size="1"><a href="#error">Eroare</a></strong></font> + + + + Describes an error that occurred when downloading + Descrie eroarea apărută în timpul descărcării + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + <strong><font size="2">ATENŢIE: Nerecomandat pentru acest hardware. Modelul necesită mai multă memorie (%1 GB) decât are acest sistem (%2).</strong></font> + + + + Error for incompatible hardware + Eroare: hardware incompatibil + + + + Download progressBar + Progresia descărcării + + + + Shows the progress made in the download + Afişează progresia descărcării + + + + Download speed + Viteza de download + + + + Download speed in bytes/kilobytes/megabytes per second + Viteza de download în bytes/kilobytes/megabytes pe secundă + + + + Calculating... + Calculare... + + + + + + + Whether the file hash is being calculated + Dacă se calculează hash-ul fişierului + + + + Displayed when the file hash is being calculated + Se afişează când se calculează hash-ul fişierului + + + + ERROR: $API_KEY is empty. + EROARE: $API_KEY absentă. + + + + enter $API_KEY + introdu cheia $API_KEY + + + + ERROR: $BASE_URL is empty. + EROARE: $BASE_URL absentă. + + + + enter $BASE_URL + introdu $BASE_URL + + + + ERROR: $MODEL_NAME is empty. + EROARE: $MODEL_NAME absent + + + + enter $MODEL_NAME + introdu $MODEL_NAME + + + + File size + Dimensiunea fişierului + + + + RAM required + RAM necesară + + + + %1 GB + %1 GB + + + + + ? + ? + + + + Parameters + Parametri + + + + Quant + Quant(ificare) + + + + Type + Tip + + + + AddHFModelView + + + Use the search to find and download models from HuggingFace. There is NO GUARANTEE that these will work. Many will require additional configuration before they can be used. + Utilizați funcția de căutare pentru a găsi și descărca modele de pe HuggingFace. NU E GARANTAT că acestea vor funcționa. Multe dintre ele vor necesita configurări suplimentare înainte de a putea fi utilizate. - + Discover and download models by keyword search... Caută şi descarcă modele după un cuvânt-cheie... - + Text field for discovering and filtering downloadable models Câmp pentru căutarea şi filtrarea modelelor ce pot fi descărcate - + + Searching · %1 + Căutare · %1 + + + Initiate model discovery and filtering Iniţiază căutarea şi filtrarea modelelor - + Triggers discovery and filtering of models Activează căutarea şi filtrarea modelelor - + Default Implicit - + Likes Likes - + Downloads Download-uri - + Recent Recent/e - + + Sort by: %1 + Ordonare după: %1 + + + Asc Asc. (A->Z) - + Desc Desc. (Z->A) - - None - Niciunul - - - - Searching · %1 - Căutare · %1 - - - - Sort by: %1 - Ordonare după: %1 - - - + Sort dir: %1 Sensul ordonării: %1 - - Limit: %1 - Límită: %1 - - - - Network error: could not retrieve %1 - Eroare de reţea: nu se poate prelua %1 - - - - - Busy indicator - Indicator de activitate + + None + Niciunul - - Displayed when the models request is ongoing - Afişat în timpul solicitării modelului + + Limit: %1 + Límită: %1 - + Model file Fişierul modelului - + Model file to be downloaded - Fişierul modelului de descărcat + Fişierul modelului ce va fi descărcat - + Description Descriere - + File description Descrierea fişierului - + Cancel Anulare - + Resume Continuare - + Download Download - + Stop/restart/start the download - Opreşte/Reporneşte/Începe descărcarea + Oprirea/Repornirea/Iniţierea descărcării - + Remove - Şterge + Şterg - + Remove model from filesystem - Şterge modelul din sistemul de fişiere + Şterg modelul din sistemul de fişiere - - + + Install Instalare - + Install online model Instalez un model din online - + <strong><font size="1"><a href="#error">Error</a></strong></font> <strong><font size="1"><a href="#error">Eroare</a></strong></font> - + + Describes an error that occurred when downloading + Descrie o eroare aparută la download + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> <strong><font size="2">ATENŢIE: Nerecomandat pentru acest hardware. Modelul necesită mai multă memorie (%1 GB) decât are acest sistem (%2).</strong></font> - - %1 GB - %1 GB + + Error for incompatible hardware + Eroare - hardware incompatibil + + + + Download progressBar + Bara de progresie a descărcării + + + + Shows the progress made in the download + Afişează progresia descărcării + + + + Download speed + Viteza de download + + + + Download speed in bytes/kilobytes/megabytes per second + Viteza de download în bytes/kilobytes/megabytes pe secundă + + + + Calculating... + Calculare... + + + + + + + Whether the file hash is being calculated + Dacă se calculează hash-ul fişierului + + + + Busy indicator + Indicator de activitate + + + + Displayed when the file hash is being calculated + Afişat la calcularea hash-ului fişierului + + + + ERROR: $API_KEY is empty. + EROARE: $API_KEY absentă + + + + enter $API_KEY + introdu cheia $API_KEY + + + + ERROR: $BASE_URL is empty. + EROARE: $BASE_URL absentă + + + + enter $BASE_URL + introdu $BASE_URL + + + + ERROR: $API_KEY is empty. + EROARE: $API_KEY absentă + + + + enter $MODEL_NAME + introdu $MODEL_NAME + + + + File size + Dimensiunea fişierului + + + + Quant + Quant(ificare) + + + + Type + Tip + + + + AddModelView + + + ← Existing Models + ← Modelele curente/instalate + + + + Explore Models + Caută modele + + + + GPT4All + GPT4All + + + + HuggingFace + HuggingFace + + + Discover and download models by keyword search... + Caută şi descarcă modele după un cuvânt-cheie... + + + Text field for discovering and filtering downloadable models + Câmp pentru căutarea şi filtrarea modelelor ce pot fi descărcate + + + Initiate model discovery and filtering + Iniţiază căutarea şi filtrarea modelelor + + + Triggers discovery and filtering of models + Activează căutarea şi filtrarea modelelor + + + Default + Implicit + + + Likes + Likes + + + Downloads + Download-uri + + + Recent + Recent/e + + + Asc + Asc. (A->Z) + + + Desc + Desc. (Z->A) + + + None + Niciunul - - - ? - ? + Searching · %1 + Căutare · %1 - - Describes an error that occurred when downloading - Descrie eroarea apărută în timpul descărcării + Sort by: %1 + Ordonare după: %1 - - Error for incompatible hardware - Eroare: hardware incompatibil + Sort dir: %1 + Sensul ordonării: %1 - - Download progressBar - Progresia descărcării + Limit: %1 + Límită: %1 - - Shows the progress made in the download - Afişează progresia descărcării + Network error: could not retrieve %1 + Eroare de reţea: nu se poate prelua %1 - - Download speed - Viteza de download + Busy indicator + Indicator de activitate - - Download speed in bytes/kilobytes/megabytes per second - Viteza de download în bytes/kilobytes/megabytes pe secundă + Displayed when the models request is ongoing + Afişat în timpul solicitării modelului - - Calculating... - Calculare... + Model file + Fişierul modelului - - - - - Whether the file hash is being calculated - Dacă se calculează hash-ul fişierului + Model file to be downloaded + Fişierul modelului de descărcat - - Displayed when the file hash is being calculated - Se afişează când se calculează hash-ul fişierului + Install online model + Instalez un model din online - - ERROR: $API_KEY is empty. - EROARE: $API_KEY absentă + %1 GB + %1 GB - - enter $API_KEY - introdu cheia $API_KEY + ? + ? - - ERROR: $BASE_URL is empty. - EROARE: $BASE_URL absentă + Shows the progress made in the download + Afişează progresia descărcării - - enter $BASE_URL - introdu $BASE_URL + Download speed + Viteza de download - - ERROR: $MODEL_NAME is empty. - EROARE: $MODEL_NAME absent + Download speed in bytes/kilobytes/megabytes per second + Viteza de download în bytes/kilobytes/megabytes pe secundă - - enter $MODEL_NAME - introdu $MODEL_NAME + enter $API_KEY + introdu cheia $API_KEY - File size - Dimensiunea fişierului + Dimensiunea fişierului - RAM required - RAM necesară + RAM necesară - Parameters - Parametri + Parametri - Quant - Quant(ificare) + Quant(ificare) - Type - Tip + Tip @@ -357,7 +680,7 @@ Application - Aplicaţie/Program + Program @@ -367,7 +690,7 @@ opt-in to share feedback/conversations - optional: partajarea (share) de comentarii/conversatii + optional: partajarea (share) de comentarii/conversaţii @@ -493,7 +816,7 @@ Generate suggested follow-up questions at the end of responses. - Generarea de întrebări pentru continuare, la finalul replicilor. + Generarea de întrebări în continuarea replicilor. @@ -557,23 +880,31 @@ - Save Chat Context - Salvarea contextului conversaţiei + Enable System Tray + Trimit pe SysTray (pe bara) + The application will minimize to the system tray when the window is closed. + Programul va fi minimizat pe bara de jos + + + Save Chat Context + Salvarea contextului conversaţiei + + Save the chat model's state to disk for faster loading. WARNING: Uses ~2GB per chat. - Salvează pe disc starea modelului pentru încărcare mai rapidă. ATENŢIE: Consumă ~2GB/conversaţie. + Salvează pe disc starea modelului pentru încărcare mai rapidă. ATENŢIE: Consumă ~2GB/conversaţie. Enable Local API Server - Activează Serverul API local + Activez Serverul API local Expose an OpenAI-Compatible server to localhost. WARNING: Results in increased resource usage. - Activează pe localhost un Server compatibil cu Open-AI. ATENŢIE: Creşte consumul de resurse. + Activează pe localhost un Server compatibil cu OpenAI. ATENŢIE: Creşte consumul de resurse. @@ -604,13 +935,13 @@ Chat - - + + New Chat Conversaţie Nouă - + Server Chat Conversaţie cu Serverul @@ -618,12 +949,12 @@ ChatAPIWorker - + ERROR: Network error occurred while connecting to the API server EROARE: Eroare de reţea - conectarea la serverul API - + ChatAPIWorker::handleFinished got HTTP Error %1 %2 ChatAPIWorker::handleFinished - eroare: HTTP Error %1 %2 @@ -648,12 +979,12 @@ Create a new chat - Creează o Conversaţie nouă + Creează o Conversaţie Select the current chat or edit the chat when in edit mode - Selectează conversaţia curentă sau editeaz-o când eşti în modul editare + Selectează conversaţia curentă sau editeaz-o în modul editare @@ -673,12 +1004,12 @@ Confirm chat deletion - CONFIRMĂ ştergerea conversaţiei + CONFIRM ştergerea conversaţiei Cancel chat deletion - ANULEAZĂ ştergerea conversaţiei + ANULEZ ştergerea conversaţiei @@ -691,35 +1022,182 @@ Lista conversaţiilor în secţiunea-sertar + + ChatItemView + + + GPT4All + GPT4All + + + + You + Tu + + + + response stopped ... + replică întreruptă... + + + + retrieving localdocs: %1 ... + se preia din LocalDocs: %1 ... + + + + searching localdocs: %1 ... + se caută în LocalDocs: %1 ... + + + + processing ... + procesare... + + + + generating response ... + se generează replica... + + + + generating questions ... + se generează întrebări... + + + + + Copy + Copiere + + + + Copy Message + Copiez mesajul + + + + Disable markdown + Dezactivez markdown + + + + Enable markdown + Activez markdown + + + + %n Source(s) + + %n Sursa + %n Surse + %n de Surse + + + + + LocalDocs + LocalDocs + + + + Edit this message? + Editez mesajul + + + + + All following messages will be permanently erased. + Toate aceste mesajevor fi şterse + + + + Redo this response? + Refă raspunsul + + + + Cannot edit chat without a loaded model. + Nu se poate edita conversaţia fără un model incărcat + + + + Cannot edit chat while the model is generating. + Nu se poate edita conversaţia când un model generează text + + + + Edit + Editare + + + + Cannot redo response without a loaded model. + Nu se poate reface un răspuns fără un model incărcat + + + + Cannot redo response while the model is generating. + Nu se poate reface un răspuns când un model generează text + + + + Redo + Refacere + + + + Like response + Imi Place râspunsul + + + + Dislike response + NU Îmi Place râspunsul + + + + Suggested follow-ups + Continuări sugerate + + + + ChatLLM + + + Your message was too long and could not be processed (%1 > %2). Please try again with something shorter. + Mesajul tău e prea lung şi nu poate fi procesat. (%1 > %2). Încearca iar cu un mesaj mai scurt + + ChatListModel - + TODAY ASTĂZI - + THIS WEEK SĂPTĂMÂNA ACEASTA - + THIS MONTH LUNA ACEASTA - + LAST SIX MONTHS ULTIMELE ŞASE LUNI - + THIS YEAR ANUL ACESTA - + LAST YEAR ANUL TRECUT @@ -727,118 +1205,140 @@ ChatView - + <h3>Warning</h3><p>%1</p> <h3>Atenţie</h3><p>%1</p> - Switch model dialog - Schimbarea modelului + Schimbarea modelului - Warn the user if they switch models, then context will be erased - Avertizează utilizatorul că la schimbarea modelului va fi şters contextul + Avertizează utilizatorul că la schimbarea modelului va fi şters contextul - + Conversation copied to clipboard. Conversaţia a fost plasată în Clipboard. - + Code copied to clipboard. Codul a fost plasat în Clipboard. - + + The entire chat will be erased. + Toată conversaţia va fi ŞTEARSĂ + + + Chat panel Secţiunea de chat - + Chat panel with options Secţiunea de chat cu opţiuni - + Reload the currently loaded model Reîncarcă modelul curent - + Eject the currently loaded model Ejectează modelul curent - + No model installed. Niciun model instalat. - + Model loading error. Eroare la încărcarea modelului. - + Waiting for model... Se aşteaptă modelul... - + Switching context... Se schimbă contextul... - + Choose a model... Selectează un model... - + Not found: %1 Absent: %1 - + The top item is the current model Primul element e modelul curent - - + LocalDocs LocalDocs - + Add documents Adaug documente - + add collections of documents to the chat adaugă Colecţii de documente la conversaţie - + Load the default model Încarcă modelul implicit - + Loads the default model which can be changed in settings Încarcă modelul implicit care poate fi stabilit în Configurare - + No Model Installed Niciun model instalat - + + Legacy prompt template needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + Vechiul Prompt-Template trebuie să fie <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">actualizat</a> în Configurare. + + + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + Nu e configurat niciun <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">model de conversaţie</a>. + + + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Modelul de conversaţie</a> nu poate lipsi. + + + + Legacy system prompt needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + Vechiul System Prompt trebuie să fie <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">actualizat</a> în Configurare. + + + <h3>Encountered an error loading model:</h3><br><i>"%1"</i><br><br>Model loading failures can happen for a variety of reasons, but the most common causes include a bad file format, an incomplete or corrupted download, the wrong file type, not enough system RAM or an incompatible model type. Here are some suggestions for resolving the problem:<br><ul><li>Ensure the model file has a compatible format and type<li>Check the model file is complete in the download folder<li>You can find the download folder in the settings dialog<li>If you've sideloaded the model ensure the file is not corrupt by checking md5sum<li>Read more about what models are supported in our <a href="https://docs.gpt4all.io/">documentation</a> for the gui<li>Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help <h3>EROARE la încărcarea modelului:</h3><br><i>"%1"</i><br><br>Astfel @@ -857,154 +1357,149 @@ se oferă ajutor - + + + Erase conversation? + ŞTERG conversaţia + + + + Changing the model will erase the current conversation. + Schimbarea modelului va ŞTERGE conversaţia curenta. + + + GPT4All requires that you install at least one model to get started GPT4All necesită cel puţin un model pentru a putea rula - + Install a Model Instalează un model - + Shows the add model view Afişează secţiunea de adăugare a unui model - + Conversation with the model Conversaţie cu modelul - + prompt / response pairs from the conversation perechi prompt/replică din conversaţie - GPT4All - GPT4All + GPT4All - You - Tu + Tu - response stopped ... - replică întreruptă... + replică întreruptă... - processing ... - procesare... + procesare... - generating response ... - se generează replica... + se generează replica... - generating questions ... - se generează întrebări... + se generează întrebări... - - + Copy Copiere - Copy Message - Copiez mesajul + Copiez mesajul - Disable markdown - Dezactivez markdown + Dezactivez markdown - Enable markdown - Activez markdown + Activez markdown - Thumbs up - Bravo + Bravo - Gives a thumbs up to the response - Dă un Bravo acestei replici + Dă un Bravo acestei replici - Thumbs down - Aiurea + Aiurea - Opens thumbs down dialog - Deschide reacţia Aiurea + Deschide reacţia Aiurea - Suggested follow-ups - Continuări sugerate + Continuări sugerate - + Erase and reset chat session Şterge şi resetează sesiunea de chat - + Copy chat session to clipboard Copiez sesiunea de chat (conversaţia) în Clipboard - Redo last chat response - Reface ultima replică + Reface ultima replică - + Add media Adaugă media (un fişier) - + Adds media to the prompt Adaugă media (un fişier) la prompt - + Stop generating Opreşte generarea - + Stop the current response generation Opreşte generarea replicii curente - + Attach Ataşează - + Single File Un singur fişier - + Reloads the model Reîncarc modelul @@ -1039,82 +1534,78 @@ model to get started se oferă ajutor - - + + Reload · %1 Reîncărcare · %1 - + Loading · %1 Încărcare · %1 - + Load · %1 (default) → Încarcă · %1 (implicit) → - restoring from text ... - restaurare din text... + restaurare din text... - retrieving localdocs: %1 ... - se preia din LocalDocs: %1 ... + se preia din LocalDocs: %1 ... - searching localdocs: %1 ... - se caută în LocalDocs: %1 ... + se caută în LocalDocs: %1 ... - %n Source(s) - + %n Sursa %n Surse %n de Surse - + Send a message... Trimite un mesaj... - + Load a model to continue... Încarcă un model pentru a continua... - + Send messages/prompts to the model Trimite mesaje/prompt-uri către model - + Cut Decupare (Cut) - + Paste Alipire (Paste) - + Select All Selectez tot - + Send message Trimit mesajul - + Sends the message/prompt contained in textfield to the model Trimite modelului mesajul/prompt-ul din câmpul-text @@ -1160,40 +1651,53 @@ model to get started Selectează o Colecţie pentru ca modelul să o poată accesa. + + ConfirmationDialog + + + OK + OK + + + + Cancel + Anulare + + Download - + Model "%1" is installed successfully. Modelul "%1" - instalat cu succes. - + ERROR: $MODEL_NAME is empty. EROARE: $MODEL_NAME absent. - + ERROR: $API_KEY is empty. EROARE: $API_KEY absentă - + ERROR: $BASE_URL is invalid. EROARE: $API_KEY incorecta - + ERROR: Model "%1 (%2)" is conflict. EROARE: Model "%1 (%2)" conflictual. - + Model "%1 (%2)" is installed successfully. Modelul "%1 (%2)" - instalat cu succes. - + Model "%1" is removed. Modelul "%1" - îndepărtat @@ -1208,7 +1712,7 @@ model to get started The privacy-first LLM chat application - Programul ce prioritizează confidenţialitatea (privacy) + Programul ce Prioritizează Confidenţialitatea (Privacy) @@ -1321,7 +1825,7 @@ model to get started Use Nomic Embed API - Folosesc Nomic Embed API + Folosesc API: Nomic Embed @@ -1359,52 +1863,52 @@ model to get started Implicit - + Display Vizualizare - + Show Sources Afişarea Surselor - + Display the sources used for each response. Afişează Sursele utilizate pentru fiecare replică. - + Advanced Avansate - + Warning: Advanced usage only. Atenţie: Numai pentru utilizare avansată. - + Values too large may cause localdocs failure, extremely slow responses or failure to respond at all. Roughly speaking, the {N chars x N snippets} are added to the model's context window. More info <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">here</a>. Valori prea mari pot cauza erori cu LocalDocs, replici foarte lente sau chiar absenţa lor. În mare, numărul {N caractere x N citate} este adăugat la Context Window/Size/Length a modelului. Mai multe informaţii: <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">aici</a>. - + Number of characters per document snippet. Larger numbers increase likelihood of factual responses, but also result in slower generation. Numărul caracterelor din fiecare citat. Numere mari amplifică probabilitatea unor replici corecte, dar de asemenea cauzează generare lentă. - + Max best N matches of retrieved document snippets to add to the context for prompt. Larger numbers increase likelihood of factual responses, but also result in slower generation. Numărul maxim al citatelor ce corespund şi care vor fi adăugate la contextul pentru prompt. Numere mari amplifică probabilitatea unor replici corecte, dar de asemenea cauzează generare lentă. - + Document snippet size (characters) Lungimea (în caractere) a citatelor din documente - + Max document snippets per prompt Numărul maxim de citate per prompt @@ -1444,7 +1948,7 @@ model to get started + Add Doc Collection - + Adaugă o Colecţie de documente + + Adaug o Colecţie de documente @@ -1469,7 +1973,7 @@ model to get started INDEXING - ...SE INDEXEAZĂ... + ...INDEXARE... @@ -1494,7 +1998,7 @@ model to get started Indexing in progress - Se Indexează... + ...Se Indexează... @@ -1504,7 +2008,7 @@ model to get started This collection requires an update after version change - Această Colecţie necesită update după schimbarea versiunii + Colecţia necesită update după schimbarea versiunii @@ -1568,78 +2072,78 @@ model to get started ModelList - - + + cannot open "%1": %2 nu se poate deschide „%1”: %2 - + cannot create "%1": %2 nu se poate crea „%1”: %2 - + %1 (%2) %1 (%2) - + <strong>OpenAI-Compatible API Model</strong><br><ul><li>API Key: %1</li><li>Base URL: %2</li><li>Model Name: %3</li></ul> <strong>Model API compatibil cu OpenAI</strong><br><ul><li>Cheia API: %1</li><li>Base URL: %2</li><li>Numele modelului: %3</li></ul> - + <ul><li>Requires personal OpenAI API key.</li><li>WARNING: Will send your chats to OpenAI!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with OpenAI</li><li>You can apply for an API key <a href="https://platform.openai.com/account/api-keys">here.</a></li> <ul><li>Necesită o cheie API OpenAI personală. </li><li>ATENŢIE: Conversaţiile tale vor fi trimise la OpenAI!</li><li>Cheia ta API va fi stocată pe disc (local) </li><li>Va fi utilizată numai pentru comunicarea cu OpenAI</li><li>Poţi solicita o cheie API aici: <a href="https://platform.openai.com/account/api-keys">aici.</a></li> - + <strong>OpenAI's ChatGPT model GPT-3.5 Turbo</strong><br> %1 <strong>Modelul OpenAI's ChatGPT GPT-3.5 Turbo</strong><br> %1 - + <br><br><i>* Even if you pay OpenAI for ChatGPT-4 this does not guarantee API key access. Contact OpenAI for more info. <br><br><i>* Chiar dacă plăteşti la OpenAI pentru ChatGPT-4, aceasta nu garantează accesul la cheia API. Contactează OpenAI pentru mai multe informaţii. - + <strong>OpenAI's ChatGPT model GPT-4</strong><br> %1 %2 <strong>Modelul ChatGPT GPT-4 al OpenAI</strong><br> %1 %2 - + <ul><li>Requires personal Mistral API key.</li><li>WARNING: Will send your chats to Mistral!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with Mistral</li><li>You can apply for an API key <a href="https://console.mistral.ai/user/api-keys">here</a>.</li> <ul><li>Necesită cheia personală Mistral API. </li><li>ATENŢIE: Conversaţiile tale vor fi trimise la Mistral!</li><li>Cheia ta API va fi stocată pe disc (local)</li><li>Va fi utilizată numai pentru comunicarea cu Mistral</li><li>Poţi solicita o cheie API aici: <a href="https://console.mistral.ai/user/api-keys">aici</a>.</li> - + <strong>Mistral Tiny model</strong><br> %1 <strong>Modelul Mistral Tiny</strong><br> %1 - + <strong>Mistral Small model</strong><br> %1 <strong>Modelul Mistral Small</strong><br> %1 - + <strong>Mistral Medium model</strong><br> %1 <strong>Modelul Mistral Medium</strong><br> %1 - + <ul><li>Requires personal API key and the API base URL.</li><li>WARNING: Will send your chats to the OpenAI-compatible API Server you specified!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with the OpenAI-compatible API Server</li> <ul><li>Necesită cheia personală API si base-URL a API.</li><li>ATENŢIE: Conversaţiile tale vor fi trimise la serverul API compatibil cu OpenAI specificat!</li><li>Cheia ta API va fi stocată pe disc (local)</li><li>Va fi utilizată numai pentru comunicarea cu serverul API compatibil cu OpenAI</li> - + <strong>Connect to OpenAI-compatible API server</strong><br> %1 <strong>Conectare la un server API compatibil cu OpenAI</strong><br> %1 - + <strong>Created by %1.</strong><br><ul><li>Published on %2.<li>This model has %3 likes.<li>This model has %4 downloads.<li>More info can be found <a href="https://huggingface.co/%5">here.</a></ul> <strong>Creat de către %1.</strong><br><ul><li>Publicat in: %2.<li>Acest model are %3 Likes.<li>Acest model are %4 download-uri.<li>Mai multe informaţii pot fi găsite la: <a href="https://huggingface.co/%5">aici.</a></ul> @@ -1652,214 +2156,302 @@ model to get started Model - + + %1 system message? + %1 mesajul de la sistem? + + + + + Clear + Ştergere + + + + + Reset + Resetare + + + + The system message will be %1. + Mesajul de la sistem va fi %1. + + + + removed + îndepărtat + + + + + reset to the default + resetare la valoarea implicită + + + + %1 chat template? + %1 modelul de conversaţie? + + + + The chat template will be %1. + Modelul de conversaţie va fi %1. + + + + erased + şters + + + Model Settings Configurez modelul - + Clone Clonez - + Remove Şterg - + Name Denumire - + Model File Fişierul modelului - System Prompt - System Prompt + System Prompt - Prompt Template - Prompt Template + Prompt Template - The template that wraps every prompt. - Standardul de formulare a fiecărui prompt. + Standardul de formulare a fiecărui prompt. - + Chat Name Prompt Denumirea conversaţiei - + Prompt used to automatically generate chat names. Standardul de formulare a denumirii conversaţiilor. - + Suggested FollowUp Prompt Prompt-ul sugerat pentru a continua - + Prompt used to generate suggested follow-up questions. Prompt-ul folosit pentru generarea întrebărilor de continuare. - + Context Length Lungimea Contextului - + Number of input and output tokens the model sees. Numărul token-urilor de input şi de output văzute de model. - + Temperature Temperatura - + Randomness of model output. Higher -> more variation. Libertatea/Confuzia din replica modelului. Mai mare -> mai multă libertate. - + Top-P Top-P - + Nucleus Sampling factor. Lower -> more predictable. Factorul de Nucleus Sampling. Mai mic -> predictibilitate mai mare. - Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens. - Plasat la începutul fiecărei conversaţii. Trebuie să conţină token-uri(le) adecvate de încadrare. + Plasat la începutul fiecărei conversaţii. Trebuie să conţină token-uri(le) adecvate de încadrare. - Must contain the string "%1" to be replaced with the user's input. - Trebuie să conţină textul "%1" care va fi înlocuit cu ceea ce scrie utilizatorul. + Trebuie să conţină textul "%1" care va fi înlocuit cu ceea ce scrie utilizatorul. - + Maximum combined prompt/response tokens before information is lost. Using more context than the model was trained on will yield poor results. NOTE: Does not take effect until you reload the model. Numărul maxim combinat al token-urilor în prompt+replică înainte de a se pierde informaţie. Utilizarea unui context mai mare decât cel cu care a fost instruit modelul va întoarce rezultate mai slabe. NOTĂ: Nu are efect până la reîncărcarea modelului. - + Temperature increases the chances of choosing less likely tokens. NOTE: Higher temperature gives more creative but less predictable outputs. Temperatura creşte probabilitatea de alegere a unor token-uri puţin probabile. NOTĂ: O temperatură tot mai înaltă determină replici tot mai creative şi mai puţin predictibile. - + Only the most likely tokens up to a total probability of top_p can be chosen. NOTE: Prevents choosing highly unlikely tokens. - Pot fi alese numai cele mai probabile token-uri a căror probabilitate totală este Top-P. NOTĂ: Se evită selectarea token-urilor foarte improbabile. + Pot fi alese numai cele mai probabile token-uri a căror probabilitate totală este Top-P. NOTĂ: Evită selectarea token-urilor foarte improbabile. - + Min-P Min-P - + Minimum token probability. Higher -> more predictable. Probabilitatea mínimă a unui token. Mai mare -> mai predictibil. - + Sets the minimum relative probability for a token to be considered. Stabileşte probabilitatea minimă relativă a unui token de luat în considerare. - + Top-K Top-K - + Size of selection pool for tokens. Dimensiunea setului de token-uri. - + Only the top K most likely tokens will be chosen from. Se va alege numai din cele mai probabile K token-uri. - + Max Length Lungimea maximă - + Maximum response length, in tokens. Lungimea maximă - în token-uri - a replicii. - + Prompt Batch Size Prompt Batch Size - + The batch size used for prompt processing. Dimensiunea setului de token-uri citite simultan din prompt. - + Amount of prompt tokens to process at once. NOTE: Higher values can speed up reading prompts but will use more RAM. Numărul token-urilor procesate simultan. NOTĂ: Valori tot mai mari pot accelera citirea prompt-urilor, dar şi utiliza mai multă RAM. - + How many model layers to load into VRAM. Decrease this if GPT4All runs out of VRAM while loading this model. Lower values increase CPU load and RAM usage, and make inference slower. NOTE: Does not take effect until you reload the model. Cât de multe layere ale modelului să fie încărcate în VRAM. Valori mici trebuie folosite dacă GPT4All rămâne fără VRAM în timp ce încarcă modelul. Valorile tot mai mici cresc utilizarea CPU şi a RAM şi încetinesc inferenţa. NOTĂ: Nu are efect până la reîncărcarea modelului. - + Repeat Penalty Penalizarea pentru repetare - + + System Message + Mesaj de la Sistem + + + + A message to set the context or guide the behavior of the model. Leave blank for none. NOTE: Since GPT4All 3.5, this should not contain control tokens. + Un mesaj pentru stabilirea contextului sau ghidarea comportamentului modelului. Poate fi nespecificat. NOTĂ: De la GPT4All 3.5, acesta nu trebuie să conţină tokenuri de control. + + + + System message is not <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">plain text</a>. + Mesajul de la Sistem nu e <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">text-simplu</a>. + + + + Chat Template + Model de conversaţie + + + + This Jinja template turns the chat into input for the model. + Acest model Jinja transformă conversaţia în input pentru model. + + + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + Nu e configurat niciun <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">model de conversaţie</a>. + + + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Modelul de conversaţie</a> nu poate lipsi. + + + + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Syntax error</a>: %1 + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Syntax error</a>: %1 + + + + Chat template is not in <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Jinja format</a>. + Modelul de conversaţie nu este in <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">format Jinja</a>. + + + Repetition penalty factor. Set to 1 to disable. Factorul de penalizare a repetării ce se dezactivează cu valoarea 1. - + Repeat Penalty Tokens Token-uri pentru penalizare a repetării - + Number of previous tokens used for penalty. Numărul token-urilor anterioare considerate pentru penalizare. - + GPU Layers Layere în GPU - + Number of model layers to load into VRAM. Numărul layerelor modelului ce vor fi Încărcate în VRAM. @@ -1895,7 +2487,7 @@ NOTE: Does not take effect until you reload the model. Locally installed chat models - Modele conversaţionale instalate local + Modele conversaţionale instalate @@ -1946,7 +2538,7 @@ NOTE: Does not take effect until you reload the model. Install - Instalează + Instalez @@ -2111,6 +2703,19 @@ NOTE: Does not take effect until you reload the model. Selectează un director (folder) + + MySettingsLabel + + + Clear + Ştergere + + + + Reset + Resetare + + MySettingsStack @@ -2121,14 +2726,24 @@ NOTE: Does not take effect until you reload the model. MySettingsTab - + + Restore defaults? + Restaurare la implicite + + + + This page of settings will be reset to the defaults. + Setările de pe această pagină vor fi resetate la valorile implicite. + + + Restore Defaults Restaurez valorile implicite - + Restores settings dialog to a default state - Restaurez secţiunea de configurare la starea sa implicită + Restaurez secţiunea Configurare la starea sa implicită @@ -2136,7 +2751,7 @@ NOTE: Does not take effect until you reload the model. Contribute data to the GPT4All Opensource Datalake. - Contribuie cu date/informaţii la componenta Open-source DataLake a GPT4All. + Contribui cu date/informaţii la componenta Open-source DataLake a GPT4All. @@ -2154,7 +2769,7 @@ NOTĂ: Dacă activezi această funcţionalitate, vei trimite datele tale la comp Terms for opt-in - Termenii pentru participare + Termenii participării @@ -2164,7 +2779,7 @@ NOTĂ: Dacă activezi această funcţionalitate, vei trimite datele tale la comp Please provide a name for attribution (optional) - Specifică o denumire pentru această apreciere (opţional) + Specifică un nume pentru această apreciere (opţional) @@ -2184,7 +2799,7 @@ NOTĂ: Dacă activezi această funcţionalitate, vei trimite datele tale la comp Enable opt-in - Activează participarea + Activez participarea @@ -2194,7 +2809,7 @@ NOTĂ: Dacă activezi această funcţionalitate, vei trimite datele tale la comp Cancel opt-in - Anulează participarea + Anulez participarea @@ -2212,7 +2827,7 @@ NOTĂ: Dacă activezi această funcţionalitate, vei trimite datele tale la comp Update to new version - Actualizează la noua versiune + Actualizez la noua versiune @@ -2363,7 +2978,7 @@ un răspuns e Aiurea. poţi sugera un răspuns alternativ. Aceste date vor fi co componenta DataLake a GPT4All. NOTĂ: Dacă activezi această funcţionalitate, vei trimite datele tale la componenta DataLake a GPT4All. -Atunci nu te vei putea aştepta la intimitatea (privacy) conversaţiei dacă activezi această funcţionalitate. +Atunci nu te vei putea aştepta la confidenţialitatea (privacy) conversaţiei dacă activezi această funcţionalitate. Totuşi, te poţi aştepta la a beneficia de apreciere - opţional, dacă doreşti. Datele din conversaţie vor fi disponibile pentru oricine vrea să le descarce şi vor fi utilizate de către Nomic AI @@ -2383,6 +2998,11 @@ care foloseşte datele tale! Describes what will happen when you opt-in Descrie ce se întâmplă când participi + + + Opt-in to anonymous usage analytics used to improve GPT4All + Optați pentru trimiterea anonimă a evidenței utilizării, folosite pentru a îmbunătăți GPT4All + @@ -2416,6 +3036,11 @@ care foloseşte datele tale! Allow opt-out for anonymous usage statistics Permite anularea participării la colectarea de statistici despre utilizare -anonimă- + + + Opt-in to anonymous sharing of chats to the GPT4All Datalake + Optați pentru partajarea anonimă a conversațiilor în GPT4All Datalake + @@ -2446,25 +3071,20 @@ care foloseşte datele tale! SwitchModelDialog - <b>Warning:</b> changing the model will erase the current conversation. Do you wish to continue? - <b>Atenţie:</b> schimbarea modelului va şterge conversaţia curentă. Confirmi aceasta? + <b>Atenţie:</b> schimbarea modelului va şterge conversaţia curentă. Confirmi aceasta? - Continue - Continuă + Continuă - Continue with model loading - Continuă încărcarea modelului + Continuă încărcarea modelului - - Cancel - Anulare + Anulare @@ -2503,125 +3123,135 @@ care foloseşte datele tale! main - + GPT4All v%1 GPT4All v%1 - + + Restore + Restaurare + + + + Quit + Abandon + + + <h3>Encountered an error starting up:</h3><br><i>"Incompatible hardware detected."</i><br><br>Unfortunately, your CPU does not meet the minimal requirements to run this program. In particular, it does not support AVX intrinsics which this program requires to successfully run a modern large language model. The only solution at this time is to upgrade your hardware to a more modern CPU.<br><br>See here for more information: <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">https://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> <h3>A apărut o eroare la iniţializare:; </h3><br><i>"Hardware incompatibil. "</i><br><br>Din păcate, procesorul (CPU) nu întruneşte condiţiile minime pentru a rula acest program. În particular, nu suportă instrucţiunile AVX pe care programul le necesită pentru a integra un model conversaţional modern. În acest moment, unica soluţie este să îţi aduci la zi sistemul hardware cu un CPU mai recent.<br><br>Aici sunt mai multe informaţii: <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">https://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> - + <h3>Encountered an error starting up:</h3><br><i>"Inability to access settings file."</i><br><br>Unfortunately, something is preventing the program from accessing the settings file. This could be caused by incorrect permissions in the local app config directory where the settings file is located. Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help. <h3>A apărut o eroare la iniţializare:; </h3><br><i>"Nu poate fi accesat fişierul de configurare a programului."</i><br><br>Din păcate, ceva împiedică programul în a accesa acel fişier. Cauza poate fi un set de permisiuni incorecte pe directorul/folderul local de configurare unde se află acel fişier. Poţi parcurge canalul nostru <a href="https://discord.gg/4M2QFmTt2k">Discord</a> unde vei putea primi asistenţă. - + Connection to datalake failed. Conectarea la DataLake a eşuat. - + Saving chats. Se salvează conversaţiile. - + Network dialog Dialogul despre reţea - + opt-in to share feedback/conversations acceptă partajarea (share) de comentarii/conversaţii - + Home view Secţiunea de Început - + Home view of application Secţiunea de Început a programului - + Home Prima<br>pagină - + Chat view Secţiunea conversaţiilor - + Chat view to interact with models Secţiunea de chat pentru interacţiune cu modele - + Chats Conversaţii - - + + Models Modele - + Models view for installed models Secţiunea modelelor instalate - - + + LocalDocs LocalDocs - + LocalDocs view to configure and use local docs Secţiunea LocalDocs de configurare şi folosire a Documentelor Locale - - + + Settings Configurare - + Settings view for application configuration - Secţiunea de configurare a programului + Secţiunea de Configurare a programului - + The datalake is enabled DataLake: ACTIV - + Using a network model Se foloseşte un model pe reţea - + Server mode is enabled Modul Server: ACTIV - + Installed models Modele instalate - + View of installed models Secţiunea modelelor instalate diff --git a/gpt4all-chat/translations/gpt4all_zh_CN.ts b/gpt4all-chat/translations/gpt4all_zh_CN.ts index f6c87a58018d..c1537e5779e0 100644 --- a/gpt4all-chat/translations/gpt4all_zh_CN.ts +++ b/gpt4all-chat/translations/gpt4all_zh_CN.ts @@ -63,6 +63,467 @@ 创建集合 + + AddGPT4AllModelView + + + These models have been specifically configured for use in GPT4All. The first few models on the list are known to work the best, but you should only attempt to use models that will fit in your available memory. + + + + + Network error: could not retrieve %1 + 网络错误:无法检索 %1 + + + + + Busy indicator + 繁忙程度 + + + + Displayed when the models request is ongoing + 在模型请求进行中时显示 + + + + Model file + 模型文件 + + + + Model file to be downloaded + + + + + Description + 描述 + + + + File description + 文件描述 + + + + Cancel + 取消 + + + + Resume + 继续 + + + + Download + 下载 + + + + Stop/restart/start the download + 停止/重启/开始下载 + + + + Remove + 删除 + + + + Remove model from filesystem + 从系统中删除模型 + + + + + Install + + + + + Install online model + 安装在线模型 + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + + + + + Describes an error that occurred when downloading + + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + + + + + Error for incompatible hardware + 硬件不兼容的错误 + + + + Download progressBar + 下载进度 + + + + Shows the progress made in the download + 显示下载进度 + + + + Download speed + 下载速度 + + + + Download speed in bytes/kilobytes/megabytes per second + 下载速度 b/kb/mb /s + + + + Calculating... + + + + + + + + Whether the file hash is being calculated + 是否正在计算文件哈希 + + + + Displayed when the file hash is being calculated + 在计算文件哈希时显示 + + + + ERROR: $API_KEY is empty. + + + + + enter $API_KEY + + + + + ERROR: $BASE_URL is empty. + 错误:$BASE_URL 为空 + + + + enter $BASE_URL + 输入 $BASE_URL + + + + ERROR: $MODEL_NAME is empty. + + + + + enter $MODEL_NAME + 输入:$MODEL_NAME + + + + File size + 文件大小 + + + + RAM required + + + + + %1 GB + %1 GB + + + + + ? + + + + + Parameters + 参数 + + + + Quant + 量化 + + + + Type + 类型 + + + + AddHFModelView + + + Use the search to find and download models from HuggingFace. There is NO GUARANTEE that these will work. Many will require additional configuration before they can be used. + + + + + Discover and download models by keyword search... + 通过关键词查找并下载模型 ... + + + + Text field for discovering and filtering downloadable models + 用于发现和筛选可下载模型的文本字段 + + + + Searching · %1 + 搜索中 · %1 + + + + Initiate model discovery and filtering + 启动模型发现和过滤 + + + + Triggers discovery and filtering of models + 触发模型的发现和筛选 + + + + Default + 默认 + + + + Likes + 喜欢 + + + + Downloads + 下载 + + + + Recent + 近期 + + + + Sort by: %1 + 排序: %1 + + + + Asc + 升序 + + + + Desc + 倒序 + + + + Sort dir: %1 + 排序目录: %1 + + + + None + + + + + Limit: %1 + 数量: %1 + + + + Model file + 模型文件 + + + + Model file to be downloaded + + + + + Description + 描述 + + + + File description + 文件描述 + + + + Cancel + 取消 + + + + Resume + 继续 + + + + Download + 下载 + + + + Stop/restart/start the download + 停止/重启/开始下载 + + + + Remove + 删除 + + + + Remove model from filesystem + 从系统中删除模型 + + + + + Install + + + + + Install online model + 安装在线模型 + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + + + + + Describes an error that occurred when downloading + + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + + + + + Error for incompatible hardware + 硬件不兼容的错误 + + + + Download progressBar + 下载进度 + + + + Shows the progress made in the download + 显示下载进度 + + + + Download speed + 下载速度 + + + + Download speed in bytes/kilobytes/megabytes per second + 下载速度 b/kb/mb /s + + + + Calculating... + + + + + + + + Whether the file hash is being calculated + 是否正在计算文件哈希 + + + + Busy indicator + 繁忙程度 + + + + Displayed when the file hash is being calculated + 在计算文件哈希时显示 + + + + ERROR: $API_KEY is empty. + + + + + enter $API_KEY + + + + + ERROR: $BASE_URL is empty. + 错误:$BASE_URL 为空 + + + + enter $BASE_URL + 输入 $BASE_URL + + + + ERROR: $MODEL_NAME is empty. + + + + + enter $MODEL_NAME + 输入:$MODEL_NAME + + + + File size + 文件大小 + + + + Quant + 量化 + + + + Type + 类型 + + AddModelView @@ -76,280 +537,230 @@ 发现模型 - + + GPT4All + GPT4All + + + + HuggingFace + + + Discover and download models by keyword search... - 通过关键词查找并下载模型 ... + 通过关键词查找并下载模型 ... - Text field for discovering and filtering downloadable models - 用于发现和筛选可下载模型的文本字段 + 用于发现和筛选可下载模型的文本字段 - Initiate model discovery and filtering - 启动模型发现和过滤 + 启动模型发现和过滤 - Triggers discovery and filtering of models - 触发模型的发现和筛选 + 触发模型的发现和筛选 - Default - 默认 + 默认 - Likes - 喜欢 + 喜欢 - Downloads - 下载 + 下载 - Recent - 近期 + 近期 - Asc - 升序 + 升序 - Desc - 倒序 + 倒序 - None - + - Searching · %1 - 搜索中 · %1 + 搜索中 · %1 - Sort by: %1 - 排序: %1 + 排序: %1 - Sort dir: %1 - 排序目录: %1 + 排序目录: %1 - Limit: %1 - 数量: %1 + 数量: %1 - Network error: could not retrieve %1 - 网络错误:无法检索 %1 + 网络错误:无法检索 %1 - - Busy indicator - 繁忙程度 + 繁忙程度 - Displayed when the models request is ongoing - 在模型请求进行中时显示 + 在模型请求进行中时显示 - Model file - 模型文件 + 模型文件 - Model file to be downloaded - 待下载模型 + 待下载模型 - Description - 描述 + 描述 - File description - 文件描述 + 文件描述 - Cancel - 取消 + 取消 - Resume - 继续 + 继续 - Download - 下载 + 下载 - Stop/restart/start the download - 停止/重启/开始下载 + 停止/重启/开始下载 - Remove - 删除 + 删除 - Remove model from filesystem - 从系统中删除模型 + 从系统中删除模型 - - Install - 安装 + 安装 - Install online model - 安装在线模型 + 安装在线模型 - <strong><font size="1"><a href="#error">Error</a></strong></font> - <strong><font size="1"><a href="#error">错误</a></strong></font> + <strong><font size="1"><a href="#error">错误</a></strong></font> - <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> - <strong><font size="2">警告: 你的设备硬件不推荐 ,模型需要的内存 (%1 GB)比你的系统还要多 (%2).</strong></font> + <strong><font size="2">警告: 你的设备硬件不推荐 ,模型需要的内存 (%1 GB)比你的系统还要多 (%2).</strong></font> - ERROR: $API_KEY is empty. - 错误:$API_KEY 为空 + 错误:$API_KEY 为空 - ERROR: $BASE_URL is empty. - 错误:$BASE_URL 为空 + 错误:$BASE_URL 为空 - enter $BASE_URL - 输入 $BASE_URL + 输入 $BASE_URL - ERROR: $MODEL_NAME is empty. - 错误:$MODEL_NAME为空 + 错误:$MODEL_NAME为空 - enter $MODEL_NAME - 输入:$MODEL_NAME + 输入:$MODEL_NAME - %1 GB - %1 GB + %1 GB - - ? - + - Describes an error that occurred when downloading - 描述下载过程中发生的错误 + 描述下载过程中发生的错误 - Error for incompatible hardware - 硬件不兼容的错误 + 硬件不兼容的错误 - Download progressBar - 下载进度 + 下载进度 - Shows the progress made in the download - 显示下载进度 + 显示下载进度 - Download speed - 下载速度 + 下载速度 - Download speed in bytes/kilobytes/megabytes per second - 下载速度 b/kb/mb /s + 下载速度 b/kb/mb /s - Calculating... - 计算中 + 计算中 - - - - Whether the file hash is being calculated - 是否正在计算文件哈希 + 是否正在计算文件哈希 - Displayed when the file hash is being calculated - 在计算文件哈希时显示 + 在计算文件哈希时显示 - enter $API_KEY - 输入$API_KEY + 输入$API_KEY - File size - 文件大小 + 文件大小 - RAM required - RAM 需要 + RAM 需要 - Parameters - 参数 + 参数 - Quant - 量化 + 量化 - Type - 类型 + 类型 @@ -556,13 +967,21 @@ - Save Chat Context - 保存对话上下文 + Enable System Tray + + The application will minimize to the system tray when the window is closed. + + + + Save Chat Context + 保存对话上下文 + + Save the chat model's state to disk for faster loading. WARNING: Uses ~2GB per chat. - 保存模型's 状态以提供更快加载速度. 警告: 需用 ~2GB 每个对话. + 保存模型's 状态以提供更快加载速度. 警告: 需用 ~2GB 每个对话. @@ -603,13 +1022,13 @@ Chat - - + + New Chat 新对话 - + Server Chat 服务器对话 @@ -617,12 +1036,12 @@ ChatAPIWorker - + ERROR: Network error occurred while connecting to the API server 错误:连接到 API 服务器时发生网络错误 - + ChatAPIWorker::handleFinished got HTTP Error %1 %2 ChatAPIWorker::handleFinished 收到 HTTP 错误 %1 %2 @@ -690,35 +1109,180 @@ 对话框中的聊天列表 + + ChatItemView + + + GPT4All + GPT4All + + + + You + + + + + response stopped ... + 响应停止... + + + + retrieving localdocs: %1 ... + 检索本地文档: %1 ... + + + + searching localdocs: %1 ... + 搜索本地文档: %1 ... + + + + processing ... + 处理中 + + + + generating response ... + 响应中... + + + + generating questions ... + 生成响应 + + + + + Copy + 复制 + + + + Copy Message + 复制内容 + + + + Disable markdown + 不允许markdown + + + + Enable markdown + 允许markdown + + + + %n Source(s) + + %n 资源 + + + + + LocalDocs + 本地文档 + + + + Edit this message? + + + + + + All following messages will be permanently erased. + + + + + Redo this response? + + + + + Cannot edit chat without a loaded model. + + + + + Cannot edit chat while the model is generating. + + + + + Edit + + + + + Cannot redo response without a loaded model. + + + + + Cannot redo response while the model is generating. + + + + + Redo + + + + + Like response + + + + + Dislike response + + + + + Suggested follow-ups + 建议的后续行动 + + + + ChatLLM + + + Your message was too long and could not be processed (%1 > %2). Please try again with something shorter. + + + ChatListModel - + TODAY 今天 - + THIS WEEK 本周 - + THIS MONTH 本月 - + LAST SIX MONTHS 半年内 - + THIS YEAR 今年内 - + LAST YEAR 去年 @@ -726,348 +1290,361 @@ ChatView - + <h3>Warning</h3><p>%1</p> <h3>警告</h3><p>%1</p> - Switch model dialog - 切换模型对话 + 切换模型对话 - Warn the user if they switch models, then context will be erased - 如果用户切换模型,则警告用户,然后上下文将被删除 + 如果用户切换模型,则警告用户,然后上下文将被删除 - + Conversation copied to clipboard. 复制对话到剪切板 - + Code copied to clipboard. 复制代码到剪切板 - + + The entire chat will be erased. + + + + Chat panel 对话面板 - + Chat panel with options 对话面板选项 - + Reload the currently loaded model 重载当前模型 - + Eject the currently loaded model 弹出当前加载的模型 - + No model installed. 没有安装模型 - + Model loading error. 模型加载错误 - + Waiting for model... 稍等片刻 - + Switching context... 切换上下文 - + Choose a model... 选择模型 - + Not found: %1 没找到: %1 - + The top item is the current model 当前模型的最佳选项 - - + LocalDocs 本地文档 - + Add documents 添加文档 - + add collections of documents to the chat 将文档集合添加到聊天中 - + Load the default model 载入默认模型 - + Loads the default model which can be changed in settings 加载默认模型,可以在设置中更改 - + No Model Installed 没有下载模型 - + GPT4All requires that you install at least one model to get started GPT4All要求您至少安装一个模型才能开始 - + Install a Model 下载模型 - + Shows the add model view 查看添加的模型 - + Conversation with the model 使用此模型对话 - + prompt / response pairs from the conversation 对话中的提示/响应对 - + + Legacy prompt template needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + + + + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + + + + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + + + + + Legacy system prompt needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + + + GPT4All - GPT4All + GPT4All - You - + - response stopped ... - 响应停止... + 响应停止... - processing ... - 处理中 + 处理中 - generating response ... - 响应中... + 响应中... - generating questions ... - 生成响应 + 生成响应 - - + Copy 复制 - Copy Message - 复制内容 + 复制内容 - Disable markdown - 不允许markdown + 不允许markdown - Enable markdown - 允许markdown + 允许markdown - Thumbs up - 点赞 + 点赞 - Gives a thumbs up to the response - 点赞响应 + 点赞响应 - Thumbs down - 点踩 + 点踩 - Opens thumbs down dialog - 打开点踩对话框 + 打开点踩对话框 - Suggested follow-ups - 建议的后续行动 + 建议的后续行动 - + Erase and reset chat session 擦除并重置聊天会话 - + Copy chat session to clipboard 复制对话到剪切板 - Redo last chat response - 重新生成上个响应 + 重新生成上个响应 - + Add media 新增媒體 - + Adds media to the prompt 將媒體加入提示中 - + Stop generating 停止生成 - + Stop the current response generation 停止当前响应 - + Attach - + Single File 單一文件 - + Reloads the model 重载模型 - + <h3>Encountered an error loading model:</h3><br><i>"%1"</i><br><br>Model loading failures can happen for a variety of reasons, but the most common causes include a bad file format, an incomplete or corrupted download, the wrong file type, not enough system RAM or an incompatible model type. Here are some suggestions for resolving the problem:<br><ul><li>Ensure the model file has a compatible format and type<li>Check the model file is complete in the download folder<li>You can find the download folder in the settings dialog<li>If you've sideloaded the model ensure the file is not corrupt by checking md5sum<li>Read more about what models are supported in our <a href="https://docs.gpt4all.io/">documentation</a> for the gui<li>Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help <h3>加载模型时遇到错误:</h3><br><i><%1></i><br><br>模型加载失败可能由多种原因引起,但最常见的原因包括文件格式错误、下载不完整或损坏、文件类型错误、系统 RAM 不足或模型类型不兼容。以下是一些解决问题的建议:<br><ul><li>确保模型文件具有兼容的格式和类型<li>检查下载文件夹中的模型文件是否完整<li>您可以在设置对话框中找到下载文件夹<li>如果您已侧载模型,请通过检查 md5sum 确保文件未损坏<li>在我们的 <a href="https://docs.gpt4all.io/">文档</a> 中了解有关 gui 支持哪些模型的更多信息<li>查看我们的 <a href="https://discord.gg/4M2QFmTt2k">discord 频道</a> 以获取帮助 - - + + + Erase conversation? + + + + + Changing the model will erase the current conversation. + + + + + Reload · %1 重载 · %1 - + Loading · %1 载入中 · %1 - + Load · %1 (default) → 载入 · %1 (默认) → - restoring from text ... - 从文本恢复中 + 从文本恢复中 - retrieving localdocs: %1 ... - 检索本地文档: %1 ... + 检索本地文档: %1 ... - searching localdocs: %1 ... - 搜索本地文档: %1 ... + 搜索本地文档: %1 ... - %n Source(s) - + %n 资源 - + Send a message... 发送消息... - + Load a model to continue... 选择模型并继续 - + Send messages/prompts to the model 发送消息/提示词给模型 - + Cut 剪切 - + Paste 粘贴 - + Select All 全选 - + Send message 发送消息 - + Sends the message/prompt contained in textfield to the model 将文本框中包含的消息/提示发送给模型 @@ -1109,40 +1686,53 @@ model to get started 选择一个集合,使其可用于聊天模型。 + + ConfirmationDialog + + + OK + + + + + Cancel + 取消 + + Download - + Model "%1" is installed successfully. 模型 "%1" 安装成功 - + ERROR: $MODEL_NAME is empty. 错误:$MODEL_NAME 为空 - + ERROR: $API_KEY is empty. 错误:$API_KEY为空 - + ERROR: $BASE_URL is invalid. 错误:$BASE_URL 非法 - + ERROR: Model "%1 (%2)" is conflict. 错误: 模型 "%1 (%2)" 有冲突. - + Model "%1 (%2)" is installed successfully. 模型 "%1 (%2)" 安装成功. - + Model "%1" is removed. 模型 "%1" 已删除. @@ -1308,52 +1898,52 @@ model to get started 程序默认 - + Display 显示 - + Show Sources 查看源码 - + Display the sources used for each response. 显示每个响应所使用的源。 - + Advanced 高级 - + Warning: Advanced usage only. 提示: 仅限高级使用。 - + Values too large may cause localdocs failure, extremely slow responses or failure to respond at all. Roughly speaking, the {N chars x N snippets} are added to the model's context window. More info <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">here</a>. 值过大可能会导致 localdocs 失败、响应速度极慢或根本无法响应。粗略地说,{N 个字符 x N 个片段} 被添加到模型的上下文窗口中。更多信息请见<a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">此处</a>。 - + Document snippet size (characters) 文档粘贴大小 (字符) - + Number of characters per document snippet. Larger numbers increase likelihood of factual responses, but also result in slower generation. 每个文档片段的字符数。较大的数值增加了事实性响应的可能性,但也会导致生成速度变慢。 - + Max document snippets per prompt 每个提示的最大文档片段数 - + Max best N matches of retrieved document snippets to add to the context for prompt. Larger numbers increase likelihood of factual responses, but also result in slower generation. 检索到的文档片段最多添加到提示上下文中的前 N 个最佳匹配项。较大的数值增加了事实性响应的可能性,但也会导致生成速度变慢。 @@ -1519,78 +2109,78 @@ model to get started ModelList - - + + cannot open "%1": %2 无法打开“%1”:%2 - + cannot create "%1": %2 无法创建“%1”:%2 - + %1 (%2) %1 (%2) - + <strong>OpenAI-Compatible API Model</strong><br><ul><li>API Key: %1</li><li>Base URL: %2</li><li>Model Name: %3</li></ul> <strong>与 OpenAI 兼容的 API 模型</strong><br><ul><li>API 密钥:%1</li><li>基本 URL:%2</li><li>模型名称:%3</li></ul> - + <ul><li>Requires personal OpenAI API key.</li><li>WARNING: Will send your chats to OpenAI!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with OpenAI</li><li>You can apply for an API key <a href="https://platform.openai.com/account/api-keys">here.</a></li> <ul><li>需要个人 OpenAI API 密钥。</li><li>警告:将把您的聊天内容发送给 OpenAI!</li><li>您的 API 密钥将存储在磁盘上</li><li>仅用于与 OpenAI 通信</li><li>您可以在此处<a href="https://platform.openai.com/account/api-keys">申请 API 密钥。</a></li> - + <strong>OpenAI's ChatGPT model GPT-3.5 Turbo</strong><br> %1 <strong>OpenAI's ChatGPT model GPT-3.5 Turbo</strong><br> %1 - + <strong>OpenAI's ChatGPT model GPT-4</strong><br> %1 %2 <strong>OpenAI's ChatGPT model GPT-4</strong><br> %1 %2 - + <strong>Mistral Tiny model</strong><br> %1 <strong>Mistral Tiny model</strong><br> %1 - + <strong>Mistral Small model</strong><br> %1 <strong>Mistral Small model</strong><br> %1 - + <strong>Mistral Medium model</strong><br> %1 <strong>Mistral Medium model</strong><br> %1 - + <ul><li>Requires personal API key and the API base URL.</li><li>WARNING: Will send your chats to the OpenAI-compatible API Server you specified!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with the OpenAI-compatible API Server</li> <ul><li>需要个人 API 密钥和 API 基本 URL。</li><li>警告:将把您的聊天内容发送到您指定的与 OpenAI 兼容的 API 服务器!</li><li>您的 API 密钥将存储在磁盘上</li><li>仅用于与与 OpenAI 兼容的 API 服务器通信</li> - + <strong>Connect to OpenAI-compatible API server</strong><br> %1 <strong>连接到与 OpenAI 兼容的 API 服务器</strong><br> %1 - + <br><br><i>* Even if you pay OpenAI for ChatGPT-4 this does not guarantee API key access. Contact OpenAI for more info. <br><br><i>* 即使您为ChatGPT-4向OpenAI付款,这也不能保证API密钥访问。联系OpenAI获取更多信息。 - + <ul><li>Requires personal Mistral API key.</li><li>WARNING: Will send your chats to Mistral!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with Mistral</li><li>You can apply for an API key <a href="https://console.mistral.ai/user/api-keys">here</a>.</li> <ul><li>Requires personal Mistral API key.</li><li>WARNING: Will send your chats to Mistral!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with Mistral</li><li>You can apply for an API key <a href="https://console.mistral.ai/user/api-keys">here</a>.</li> - + <strong>Created by %1.</strong><br><ul><li>Published on %2.<li>This model has %3 likes.<li>This model has %4 downloads.<li>More info can be found <a href="https://huggingface.co/%5">here.</a></ul> <strong>Created by %1.</strong><br><ul><li>Published on %2.<li>This model has %3 likes.<li>This model has %4 downloads.<li>More info can be found <a href="https://huggingface.co/%5">here.</a></ul> @@ -1603,87 +2193,175 @@ model to get started 模型 - + + %1 system message? + + + + + + Clear + + + + + + Reset + + + + + The system message will be %1. + + + + + removed + + + + + + reset to the default + + + + + %1 chat template? + + + + + The chat template will be %1. + + + + + erased + + + + Model Settings 模型设置 - + Clone 克隆 - + Remove 删除 - + Name 名称 - + Model File 模型文件 - System Prompt - 系统提示词 + 系统提示词 - Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens. - 每次对话开始时的前缀 + 每次对话开始时的前缀 - Prompt Template - 提示词模版 + 提示词模版 - The template that wraps every prompt. - 包装每个提示的模板 + 包装每个提示的模板 - Must contain the string "%1" to be replaced with the user's input. - 必须包含字符串 "%1" 替换为用户的's 输入. + 必须包含字符串 "%1" 替换为用户的's 输入. + + + + System Message + + + + + A message to set the context or guide the behavior of the model. Leave blank for none. NOTE: Since GPT4All 3.5, this should not contain control tokens. + + + + + System message is not <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">plain text</a>. + + + + + Chat Template + + + + + This Jinja template turns the chat into input for the model. + + + + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + + + + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + + + + + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Syntax error</a>: %1 + + + + + Chat template is not in <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Jinja format</a>. + - + Chat Name Prompt 聊天名称提示 - + Prompt used to automatically generate chat names. 用于自动生成聊天名称的提示。 - + Suggested FollowUp Prompt 建议的后续提示 - + Prompt used to generate suggested follow-up questions. 用于生成建议的后续问题的提示。 - + Context Length 上下文长度 - + Number of input and output tokens the model sees. 模型看到的输入和输出令牌的数量。 - + Maximum combined prompt/response tokens before information is lost. Using more context than the model was trained on will yield poor results. NOTE: Does not take effect until you reload the model. @@ -1692,128 +2370,128 @@ NOTE: Does not take effect until you reload the model. 注意:在重新加载模型之前不会生效。 - + Temperature 温度 - + Randomness of model output. Higher -> more variation. 模型输出的随机性。更高->更多的变化。 - + Temperature increases the chances of choosing less likely tokens. NOTE: Higher temperature gives more creative but less predictable outputs. 温度增加了选择不太可能的token的机会。 注:温度越高,输出越有创意,但预测性越低。 - + Top-P Top-P - + Nucleus Sampling factor. Lower -> more predictable. 核子取样系数。较低->更具可预测性。 - + Only the most likely tokens up to a total probability of top_p can be chosen. NOTE: Prevents choosing highly unlikely tokens. 只能选择总概率高达top_p的最有可能的令牌。 注意:防止选择极不可能的token。 - + Min-P Min-P - + Minimum token probability. Higher -> more predictable. 最小令牌概率。更高 -> 更可预测。 - + Sets the minimum relative probability for a token to be considered. 设置被考虑的标记的最小相对概率。 - + Top-K Top-K - + Size of selection pool for tokens. 令牌选择池的大小。 - + Only the top K most likely tokens will be chosen from. 仅从最可能的前 K 个标记中选择 - + Max Length 最大长度 - + Maximum response length, in tokens. 最大响应长度(以令牌为单位) - + Prompt Batch Size 提示词大小 - + The batch size used for prompt processing. 用于快速处理的批量大小。 - + Amount of prompt tokens to process at once. NOTE: Higher values can speed up reading prompts but will use more RAM. 一次要处理的提示令牌数量。 注意:较高的值可以加快读取提示,但会使用更多的RAM。 - + Repeat Penalty 重复惩罚 - + Repetition penalty factor. Set to 1 to disable. 重复处罚系数。设置为1可禁用。 - + Repeat Penalty Tokens 重复惩罚数 - + Number of previous tokens used for penalty. 用于惩罚的先前令牌数量。 - + GPU Layers GPU 层 - + Number of model layers to load into VRAM. 要加载到VRAM中的模型层数。 - + How many model layers to load into VRAM. Decrease this if GPT4All runs out of VRAM while loading this model. Lower values increase CPU load and RAM usage, and make inference slower. NOTE: Does not take effect until you reload the model. @@ -2069,6 +2747,19 @@ NOTE: Does not take effect until you reload the model. 請選擇目錄 + + MySettingsLabel + + + Clear + + + + + Reset + + + MySettingsStack @@ -2079,12 +2770,22 @@ NOTE: Does not take effect until you reload the model. MySettingsTab - + + Restore defaults? + + + + + This page of settings will be reset to the defaults. + + + + Restore Defaults 恢复初始化 - + Restores settings dialog to a default state 将设置对话框恢复为默认状态 @@ -2283,6 +2984,11 @@ model release that uses your data! Describes what will happen when you opt-in 描述选择加入时会发生的情况 + + + Opt-in to anonymous usage analytics used to improve GPT4All + + @@ -2316,6 +3022,11 @@ model release that uses your data! Allow opt-out for anonymous usage statistics 允许选择退出匿名使用统计数据 + + + Opt-in to anonymous sharing of chats to the GPT4All Datalake + + @@ -2346,25 +3057,20 @@ model release that uses your data! SwitchModelDialog - <b>Warning:</b> changing the model will erase the current conversation. Do you wish to continue? - <b>警告:</b> 更改模型将删除当前对话。您想继续吗? + <b>警告:</b> 更改模型将删除当前对话。您想继续吗? - Continue - 继续 + 继续 - Continue with model loading - 模型载入时继续 + 模型载入时继续 - - Cancel - 取消 + 取消 @@ -2403,125 +3109,135 @@ model release that uses your data! main - + GPT4All v%1 GPT4All v%1 - + + Restore + + + + + Quit + + + + <h3>Encountered an error starting up:</h3><br><i>"Incompatible hardware detected."</i><br><br>Unfortunately, your CPU does not meet the minimal requirements to run this program. In particular, it does not support AVX intrinsics which this program requires to successfully run a modern large language model. The only solution at this time is to upgrade your hardware to a more modern CPU.<br><br>See here for more information: <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">https://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> <h3>启动时遇到错误:</h3><br><i>“检测到不兼容的硬件。”</i><br><br>很遗憾,您的 CPU 不满足运行此程序的最低要求。特别是,它不支持此程序成功运行现代大型语言模型所需的 AVX 内在函数。目前唯一的解决方案是将您的硬件升级到更现代的 CPU。<br><br>有关更多信息,请参阅此处:<a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions>>https://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> - + <h3>Encountered an error starting up:</h3><br><i>"Inability to access settings file."</i><br><br>Unfortunately, something is preventing the program from accessing the settings file. This could be caused by incorrect permissions in the local app config directory where the settings file is located. Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help. <h3>启动时遇到错误:</h3><br><i>“无法访问设置文件。”</i><br><br>不幸的是,某些东西阻止程序访问设置文件。这可能是由于设置文件所在的本地应用程序配置目录中的权限不正确造成的。请查看我们的<a href="https://discord.gg/4M2QFmTt2k">discord 频道</a> 以获取帮助。 - + Connection to datalake failed. 链接数据湖失败 - + Saving chats. 保存对话 - + Network dialog 网络对话 - + opt-in to share feedback/conversations 选择加入以共享反馈/对话 - + Home view 主页 - + Home view of application 主页 - + Home 主页 - + Chat view 对话视图 - + Chat view to interact with models 聊天视图可与模型互动 - + Chats 对话 - - + + Models 模型 - + Models view for installed models 已安装模型的页面 - - + + LocalDocs 本地文档 - + LocalDocs view to configure and use local docs LocalDocs视图可配置和使用本地文档 - - + + Settings 设置 - + Settings view for application configuration 设置页面 - + The datalake is enabled 数据湖已开启 - + Using a network model 使用联网模型 - + Server mode is enabled 服务器模式已开 - + Installed models 安装模型 - + View of installed models 查看已安装模型 diff --git a/gpt4all-chat/translations/gpt4all_zh_TW.ts b/gpt4all-chat/translations/gpt4all_zh_TW.ts index 5ba14d749e56..2e7e8eb90023 100644 --- a/gpt4all-chat/translations/gpt4all_zh_TW.ts +++ b/gpt4all-chat/translations/gpt4all_zh_TW.ts @@ -59,6 +59,467 @@ 建立收藏 + + AddGPT4AllModelView + + + These models have been specifically configured for use in GPT4All. The first few models on the list are known to work the best, but you should only attempt to use models that will fit in your available memory. + + + + + Network error: could not retrieve %1 + 網路錯誤:無法取得 %1 + + + + + Busy indicator + 忙線指示器 + + + + Displayed when the models request is ongoing + 當模型請求正在進行時顯示 + + + + Model file + 模型檔案 + + + + Model file to be downloaded + 即將下載的模型檔案 + + + + Description + 描述 + + + + File description + 檔案描述 + + + + Cancel + 取消 + + + + Resume + 恢復 + + + + Download + 下載 + + + + Stop/restart/start the download + 停止/重啟/開始下載 + + + + Remove + 移除 + + + + Remove model from filesystem + 從檔案系統移除模型 + + + + + Install + 安裝 + + + + Install online model + 安裝線上模型 + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + <strong><font size="1"><a href="#error">錯誤</a></strong></font> + + + + Describes an error that occurred when downloading + 解釋下載時發生的錯誤 + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + <strong><font size="2">警告:不推薦在您的硬體上運作。模型需要比較多的記憶體(%1 GB),但您的系統記憶體空間不足(%2)。</strong></font> + + + + Error for incompatible hardware + 錯誤,不相容的硬體 + + + + Download progressBar + 下載進度條 + + + + Shows the progress made in the download + 顯示下載進度 + + + + Download speed + 下載速度 + + + + Download speed in bytes/kilobytes/megabytes per second + 下載速度每秒 bytes/kilobytes/megabytes + + + + Calculating... + 計算中...... + + + + + + + Whether the file hash is being calculated + 是否正在計算檔案雜湊 + + + + Displayed when the file hash is being calculated + 計算檔案雜湊值時顯示 + + + + ERROR: $API_KEY is empty. + 錯誤:$API_KEY 未填寫。 + + + + enter $API_KEY + 請輸入 $API_KEY + + + + ERROR: $BASE_URL is empty. + 錯誤:$BASE_URL 未填寫。 + + + + enter $BASE_URL + 請輸入 $BASE_URL + + + + ERROR: $MODEL_NAME is empty. + 錯誤:$MODEL_NAME 未填寫。 + + + + enter $MODEL_NAME + 請輸入 $MODEL_NAME + + + + File size + 檔案大小 + + + + RAM required + 所需的記憶體 + + + + %1 GB + %1 GB + + + + + ? + + + + + Parameters + 參數 + + + + Quant + 量化 + + + + Type + 類型 + + + + AddHFModelView + + + Use the search to find and download models from HuggingFace. There is NO GUARANTEE that these will work. Many will require additional configuration before they can be used. + + + + + Discover and download models by keyword search... + 透過關鍵字搜尋探索並下載模型...... + + + + Text field for discovering and filtering downloadable models + 用於探索與過濾可下載模型的文字字段 + + + + Searching · %1 + 搜尋 · %1 + + + + Initiate model discovery and filtering + 探索與過濾模型 + + + + Triggers discovery and filtering of models + 觸發探索與過濾模型 + + + + Default + 預設 + + + + Likes + + + + + Downloads + 下載次數 + + + + Recent + 最新 + + + + Sort by: %1 + 排序依據:%1 + + + + Asc + 升序 + + + + Desc + 降序 + + + + Sort dir: %1 + 排序順序:%1 + + + + None + + + + + Limit: %1 + 上限:%1 + + + + Model file + 模型檔案 + + + + Model file to be downloaded + 即將下載的模型檔案 + + + + Description + 描述 + + + + File description + 檔案描述 + + + + Cancel + 取消 + + + + Resume + 恢復 + + + + Download + 下載 + + + + Stop/restart/start the download + 停止/重啟/開始下載 + + + + Remove + 移除 + + + + Remove model from filesystem + 從檔案系統移除模型 + + + + + Install + 安裝 + + + + Install online model + 安裝線上模型 + + + + <strong><font size="1"><a href="#error">Error</a></strong></font> + <strong><font size="1"><a href="#error">錯誤</a></strong></font> + + + + Describes an error that occurred when downloading + 解釋下載時發生的錯誤 + + + + <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> + <strong><font size="2">警告:不推薦在您的硬體上運作。模型需要比較多的記憶體(%1 GB),但您的系統記憶體空間不足(%2)。</strong></font> + + + + Error for incompatible hardware + 錯誤,不相容的硬體 + + + + Download progressBar + 下載進度條 + + + + Shows the progress made in the download + 顯示下載進度 + + + + Download speed + 下載速度 + + + + Download speed in bytes/kilobytes/megabytes per second + 下載速度每秒 bytes/kilobytes/megabytes + + + + Calculating... + 計算中...... + + + + + + + Whether the file hash is being calculated + 是否正在計算檔案雜湊 + + + + Busy indicator + 忙線指示器 + + + + Displayed when the file hash is being calculated + 計算檔案雜湊值時顯示 + + + + ERROR: $API_KEY is empty. + 錯誤:$API_KEY 未填寫。 + + + + enter $API_KEY + 請輸入 $API_KEY + + + + ERROR: $BASE_URL is empty. + 錯誤:$BASE_URL 未填寫。 + + + + enter $BASE_URL + 請輸入 $BASE_URL + + + + ERROR: $MODEL_NAME is empty. + 錯誤:$MODEL_NAME 未填寫。 + + + + enter $MODEL_NAME + 請輸入 $MODEL_NAME + + + + File size + 檔案大小 + + + + Quant + 量化 + + + + Type + 類型 + + AddModelView @@ -72,281 +533,231 @@ 探索模型 - + + GPT4All + GPT4All + + + + HuggingFace + + + Discover and download models by keyword search... - 透過關鍵字搜尋探索並下載模型...... + 透過關鍵字搜尋探索並下載模型...... - Text field for discovering and filtering downloadable models - 用於探索與過濾可下載模型的文字字段 + 用於探索與過濾可下載模型的文字字段 - Searching · %1 - 搜尋 · %1 + 搜尋 · %1 - Initiate model discovery and filtering - 探索與過濾模型 + 探索與過濾模型 - Triggers discovery and filtering of models - 觸發探索與過濾模型 + 觸發探索與過濾模型 - Default - 預設 + 預設 - Likes - + - Downloads - 下載次數 + 下載次數 - Recent - 最新 + 最新 - Sort by: %1 - 排序依據:%1 + 排序依據:%1 - Asc - 升序 + 升序 - Desc - 降序 + 降序 - Sort dir: %1 - 排序順序:%1 + 排序順序:%1 - None - + - Limit: %1 - 上限:%1 + 上限:%1 - Network error: could not retrieve %1 - 網路錯誤:無法取得 %1 + 網路錯誤:無法取得 %1 - <strong><font size="1"><a href="#error">Error</a></strong></font> - <strong><font size="1"><a href="#error">錯誤</a></strong></font> + <strong><font size="1"><a href="#error">錯誤</a></strong></font> - <strong><font size="2">WARNING: Not recommended for your hardware. Model requires more memory (%1 GB) than your system has available (%2).</strong></font> - <strong><font size="2">警告:不推薦在您的硬體上運作。模型需要比較多的記憶體(%1 GB),但您的系統記憶體空間不足(%2)。</strong></font> + <strong><font size="2">警告:不推薦在您的硬體上運作。模型需要比較多的記憶體(%1 GB),但您的系統記憶體空間不足(%2)。</strong></font> - %1 GB - %1 GB + %1 GB - - ? - + - - Busy indicator 參考自 https://terms.naer.edu.tw - 忙線指示器 + 忙線指示器 - Displayed when the models request is ongoing - 當模型請求正在進行時顯示 + 當模型請求正在進行時顯示 - Model file - 模型檔案 + 模型檔案 - Model file to be downloaded - 即將下載的模型檔案 + 即將下載的模型檔案 - Description - 描述 + 描述 - File description - 檔案描述 + 檔案描述 - Cancel - 取消 + 取消 - Resume - 恢復 + 恢復 - Download - 下載 + 下載 - Stop/restart/start the download - 停止/重啟/開始下載 + 停止/重啟/開始下載 - Remove - 移除 + 移除 - Remove model from filesystem - 從檔案系統移除模型 + 從檔案系統移除模型 - - Install - 安裝 + 安裝 - Install online model - 安裝線上模型 + 安裝線上模型 - Describes an error that occurred when downloading - 解釋下載時發生的錯誤 + 解釋下載時發生的錯誤 - Error for incompatible hardware - 錯誤,不相容的硬體 + 錯誤,不相容的硬體 - Download progressBar - 下載進度條 + 下載進度條 - Shows the progress made in the download - 顯示下載進度 + 顯示下載進度 - Download speed - 下載速度 + 下載速度 - Download speed in bytes/kilobytes/megabytes per second - 下載速度每秒 bytes/kilobytes/megabytes + 下載速度每秒 bytes/kilobytes/megabytes - Calculating... - 計算中...... + 計算中...... - - - - Whether the file hash is being calculated - 是否正在計算檔案雜湊 + 是否正在計算檔案雜湊 - Displayed when the file hash is being calculated - 計算檔案雜湊值時顯示 + 計算檔案雜湊值時顯示 - ERROR: $API_KEY is empty. - 錯誤:$API_KEY 未填寫。 + 錯誤:$API_KEY 未填寫。 - enter $API_KEY - 請輸入 $API_KEY + 請輸入 $API_KEY - ERROR: $BASE_URL is empty. - 錯誤:$BASE_URL 未填寫。 + 錯誤:$BASE_URL 未填寫。 - enter $BASE_URL - 請輸入 $BASE_URL + 請輸入 $BASE_URL - ERROR: $MODEL_NAME is empty. - 錯誤:$MODEL_NAME 未填寫。 + 錯誤:$MODEL_NAME 未填寫。 - enter $MODEL_NAME - 請輸入 $MODEL_NAME + 請輸入 $MODEL_NAME - File size - 檔案大小 + 檔案大小 - RAM required - 所需的記憶體 + 所需的記憶體 - Parameters - 參數 + 參數 - Quant - 量化 + 量化 - Type - 類型 + 類型 @@ -481,6 +892,16 @@ Never 永不 + + + Enable System Tray + + + + + The application will minimize to the system tray when the window is closed. + + Enable Local API Server @@ -553,14 +974,12 @@ 用於推理與嵌入的中央處理器線程數。 - Save Chat Context - 儲存交談語境 + 儲存交談語境 - Save the chat model's state to disk for faster loading. WARNING: Uses ~2GB per chat. - 將交談模型的狀態儲存到磁碟以加快載入速度。警告:每次交談使用約 2GB。 + 將交談模型的狀態儲存到磁碟以加快載入速度。警告:每次交談使用約 2GB。 @@ -596,13 +1015,13 @@ Chat - - + + New Chat 新的交談 - + Server Chat 伺服器交談 @@ -610,12 +1029,12 @@ ChatAPIWorker - + ERROR: Network error occurred while connecting to the API server 錯誤:網路錯誤,無法連線到目標 API 伺服器 - + ChatAPIWorker::handleFinished got HTTP Error %1 %2 ChatAPIWorker::handleFinished 遇到一個 HTTP 錯誤 %1 %2 @@ -683,35 +1102,180 @@ 側邊欄對話視窗的交談列表 + + ChatItemView + + + GPT4All + GPT4All + + + + You + + + + + response stopped ... + 回覆停止...... + + + + retrieving localdocs: %1 ... + 檢索本機文件中:%1 ...... + + + + searching localdocs: %1 ... + 搜尋本機文件中:%1 ...... + + + + processing ... + 處理中...... + + + + generating response ... + 生成回覆...... + + + + generating questions ... + 生成問題...... + + + + + Copy + 複製 + + + + Copy Message + 複製訊息 + + + + Disable markdown + 停用 Markdown + + + + Enable markdown + 啟用 Markdown + + + + %n Source(s) + + %n 來源 + + + + + LocalDocs + 我的文件 + + + + Edit this message? + + + + + + All following messages will be permanently erased. + + + + + Redo this response? + + + + + Cannot edit chat without a loaded model. + + + + + Cannot edit chat while the model is generating. + + + + + Edit + + + + + Cannot redo response without a loaded model. + + + + + Cannot redo response while the model is generating. + + + + + Redo + + + + + Like response + + + + + Dislike response + + + + + Suggested follow-ups + 後續建議 + + + + ChatLLM + + + Your message was too long and could not be processed (%1 > %2). Please try again with something shorter. + + + ChatListModel - + TODAY 今天 - + THIS WEEK 這星期 - + THIS MONTH 這個月 - + LAST SIX MONTHS 前六個月 - + THIS YEAR 今年 - + LAST YEAR 去年 @@ -719,349 +1283,362 @@ ChatView - + <h3>Warning</h3><p>%1</p> <h3>警告</h3><p>%1</p> - Switch model dialog - 切換模型對話視窗 + 切換模型對話視窗 - Warn the user if they switch models, then context will be erased - 警告使用者如果切換模型,則語境將被刪除 + 警告使用者如果切換模型,則語境將被刪除 - + Conversation copied to clipboard. 對話已複製到剪貼簿。 - + Code copied to clipboard. 程式碼已複製到剪貼簿。 - + + The entire chat will be erased. + + + + Chat panel 交談面板 - + Chat panel with options 具有選項的交談面板 - + Reload the currently loaded model 重新載入目前已載入的模型 - + Eject the currently loaded model 彈出目前載入的模型 - + No model installed. 沒有已安裝的模型。 - + Model loading error. 模型載入時發生錯誤。 - + Waiting for model... 等待模型中...... - + Switching context... 切換語境中...... - + Choose a model... 選擇一個模型...... - + Not found: %1 不存在:%1 - - + + Reload · %1 重新載入 · %1 - + Loading · %1 載入中 · %1 - + Load · %1 (default) → 載入 · %1 (預設) → - + + Legacy prompt template needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + + + + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + + + + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + + + + + Legacy system prompt needs to be <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">updated</a> in Settings. + + + + The top item is the current model 最上面的那項是目前使用的模型 - - + + + Erase conversation? + + + + + Changing the model will erase the current conversation. + + + + LocalDocs 我的文件 - + Add documents 新增文件 - + add collections of documents to the chat 將文件集合新增至交談中 - + Load the default model 載入預設模型 - + Loads the default model which can be changed in settings 預設模型可於設定中變更 - + No Model Installed 沒有已安裝的模型 - + GPT4All requires that you install at least one model to get started GPT4All 要求您至少安裝一個 模型開始 - + Install a Model 安裝一個模型 - + Shows the add model view 顯示新增模型視圖 - + Conversation with the model 與模型對話 - + prompt / response pairs from the conversation 對話中的提示詞 / 回覆組合 - GPT4All - GPT4All + GPT4All - You - + - response stopped ... - 回覆停止...... + 回覆停止...... - retrieving localdocs: %1 ... - 檢索本機文件中:%1 ...... + 檢索本機文件中:%1 ...... - searching localdocs: %1 ... - 搜尋本機文件中:%1 ...... + 搜尋本機文件中:%1 ...... - processing ... - 處理中...... + 處理中...... - generating response ... - 生成回覆...... + 生成回覆...... - generating questions ... - 生成問題...... + 生成問題...... - - + Copy 複製 - Copy Message - 複製訊息 + 複製訊息 - Disable markdown - 停用 Markdown + 停用 Markdown - Enable markdown - 啟用 Markdown + 啟用 Markdown - Thumbs up - + - Gives a thumbs up to the response - 對這則回覆比讚 + 對這則回覆比讚 - Thumbs down - 倒讚 + 倒讚 - Opens thumbs down dialog - 開啟倒讚對話視窗 + 開啟倒讚對話視窗 - Suggested follow-ups - 後續建議 + 後續建議 - + Erase and reset chat session 刪除並重置交談會話 - + Copy chat session to clipboard 複製交談會議到剪貼簿 - Redo last chat response - 復原上一個交談回覆 + 復原上一個交談回覆 - + Add media 附加媒體文件 - + Adds media to the prompt 附加媒體文件到提示詞 - + Stop generating 停止生成 - + Stop the current response generation 停止當前回覆生成 - + Attach 附加 - + Single File 單一文件 - + Reloads the model 重新載入模型 - + <h3>Encountered an error loading model:</h3><br><i>"%1"</i><br><br>Model loading failures can happen for a variety of reasons, but the most common causes include a bad file format, an incomplete or corrupted download, the wrong file type, not enough system RAM or an incompatible model type. Here are some suggestions for resolving the problem:<br><ul><li>Ensure the model file has a compatible format and type<li>Check the model file is complete in the download folder<li>You can find the download folder in the settings dialog<li>If you've sideloaded the model ensure the file is not corrupt by checking md5sum<li>Read more about what models are supported in our <a href="https://docs.gpt4all.io/">documentation</a> for the gui<li>Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help <h3>載入模型時發生錯誤:</h3><br><i>"%1"</i><br><br>導致模型載入失敗的原因可能有很多種,但絕大多數的原因是檔案格式損毀、下載的檔案不完整、檔案類型錯誤、系統RAM空間不足或不相容的模型類型。這裡有些建議可供疑難排解:<br><ul><li>確保使用的模型是相容的格式與類型<li>檢查位於下載資料夾的檔案是否完整<li>您可以從設定中找到您所設定的「下載資料夾路徑」<li>如果您有側載模型,請利用 md5sum 等工具確保您的檔案是完整的<li>想了解更多關於我們所支援的模型資訊,煩請詳閱<a href="https://docs.gpt4all.io/">本文件</a>。<li>歡迎洽詢我們的 <a href="https://discord.gg/4M2QFmTt2k">Discord 伺服器</a> 以尋求幫助 - restoring from text ... - 從文字中恢復...... + 從文字中恢復...... - %n Source(s) - + %n 來源 - + Send a message... 傳送一則訊息...... - + Load a model to continue... 載入模型以繼續...... - + Send messages/prompts to the model 向模型傳送訊息/提示詞 - + Cut 剪下 - + Paste 貼上 - + Select All 全選 - + Send message 傳送訊息 - + Sends the message/prompt contained in textfield to the model 將文字欄位中包含的訊息/提示詞傳送到模型 @@ -1103,40 +1680,53 @@ model to get started 選擇一個收藏以使其可供交談模型使用。 + + ConfirmationDialog + + + OK + + + + + Cancel + 取消 + + Download - + Model "%1" is installed successfully. 模型「%1」已安裝成功。 - + ERROR: $MODEL_NAME is empty. 錯誤:$MODEL_NAME 未填寫。 - + ERROR: $API_KEY is empty. 錯誤:$API_KEY 未填寫。 - + ERROR: $BASE_URL is invalid. 錯誤:$BASE_URL 無效。 - + ERROR: Model "%1 (%2)" is conflict. 錯誤:模型「%1 (%2)」發生衝突。 - + Model "%1 (%2)" is installed successfully. 模型「%1(%2)」已安裝成功。 - + Model "%1" is removed. 模型「%1」已移除。 @@ -1302,52 +1892,52 @@ model to get started 應用程式預設值 - + Display 顯示 - + Show Sources 查看來源 - + Display the sources used for each response. 顯示每則回覆所使用的來源。 - + Advanced 進階 - + Warning: Advanced usage only. 警告:僅限進階使用。 - + Values too large may cause localdocs failure, extremely slow responses or failure to respond at all. Roughly speaking, the {N chars x N snippets} are added to the model's context window. More info <a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">here</a>. 設定太大的數值可能會導致「我的文件」處理失敗、反應速度極慢或根本無法回覆。簡單地說,這會將 {N 個字元 x N 個片段} 被添加到模型的語境視窗中。更多資訊<a href="https://docs.gpt4all.io/gpt4all_desktop/localdocs.html">此處</a>。 - + Document snippet size (characters) 文件片段大小(字元) - + Number of characters per document snippet. Larger numbers increase likelihood of factual responses, but also result in slower generation. 每個文件片段的字元數。較大的數字會增加實際反應的可能性,但也會導致生成速度變慢。 - + Max document snippets per prompt 每個提示詞的最大文件片段 - + Max best N matches of retrieved document snippets to add to the context for prompt. Larger numbers increase likelihood of factual responses, but also result in slower generation. 新增至提示詞語境中的檢索到的文件片段的最大 N 個符合的項目。較大的數字會增加實際反應的可能性,但也會導致生成速度變慢。 @@ -1507,78 +2097,78 @@ model to get started ModelList - - + + cannot open "%1": %2 無法開啟“%1”:%2 - + cannot create "%1": %2 無法建立“%1”:%2 - + %1 (%2) %1(%2) - + <strong>OpenAI-Compatible API Model</strong><br><ul><li>API Key: %1</li><li>Base URL: %2</li><li>Model Name: %3</li></ul> <strong>OpenAI API 相容模型</strong><br><ul><li>API 金鑰:%1</li><li>基底 URL:%2</li><li>模型名稱:%3</li></ul> - + <ul><li>Requires personal OpenAI API key.</li><li>WARNING: Will send your chats to OpenAI!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with OpenAI</li><li>You can apply for an API key <a href="https://platform.openai.com/account/api-keys">here.</a></li> <ul><li>需要個人的 OpenAI API 金鑰。</li><li>警告:這將會傳送您的交談紀錄到 OpenAI</li><li>您的 API 金鑰將被儲存在硬碟上</li><li>它只被用於與 OpenAI 進行通訊</li><li>您可以在<a href="https://platform.openai.com/account/api-keys">此處</a>申請一個 API 金鑰。</li> - + <strong>OpenAI's ChatGPT model GPT-3.5 Turbo</strong><br> %1 <strong>OpenAI 的 ChatGPT 模型 GPT-3.5 Turbo</strong><br> %1 - + <br><br><i>* Even if you pay OpenAI for ChatGPT-4 this does not guarantee API key access. Contact OpenAI for more info. <br><br><i>* 即使您已向 OpenAI 付費購買了 ChatGPT 的 GPT-4 模型使用權,但這也不能保證您能擁有 API 金鑰的使用權限。請聯繫 OpenAI 以查閱更多資訊。 - + <strong>OpenAI's ChatGPT model GPT-4</strong><br> %1 %2 <strong>OpenAI 的 ChatGPT 模型 GPT-4</strong><br> %1 %2 - + <ul><li>Requires personal Mistral API key.</li><li>WARNING: Will send your chats to Mistral!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with Mistral</li><li>You can apply for an API key <a href="https://console.mistral.ai/user/api-keys">here</a>.</li> <ul><li>需要個人的 Mistral API 金鑰。</li><li>警告:這將會傳送您的交談紀錄到 Mistral!</li><li>您的 API 金鑰將被儲存在硬碟上</li><li>它只被用於與 Mistral 進行通訊</li><li>您可以在<a href="https://console.mistral.ai/user/api-keys">此處</a>申請一個 API 金鑰。</li> - + <strong>Mistral Tiny model</strong><br> %1 <strong>Mistral 迷你模型</strong><br> %1 - + <strong>Mistral Small model</strong><br> %1 <strong>Mistral 小型模型</strong><br> %1 - + <strong>Mistral Medium model</strong><br> %1 <strong>Mistral 中型模型</strong><br> %1 - + <ul><li>Requires personal API key and the API base URL.</li><li>WARNING: Will send your chats to the OpenAI-compatible API Server you specified!</li><li>Your API key will be stored on disk</li><li>Will only be used to communicate with the OpenAI-compatible API Server</li> <ul><li>需要個人的 API 金鑰和 API 的基底 URL(Base URL)。</li><li>警告:這將會傳送您的交談紀錄到您所指定的 OpenAI API 相容伺服器</li><li>您的 API 金鑰將被儲存在硬碟上</li><li>它只被用於與其 OpenAI API 相容伺服器進行通訊</li> - + <strong>Connect to OpenAI-compatible API server</strong><br> %1 <strong>連線到 OpenAI API 相容伺服器</strong><br> %1 - + <strong>Created by %1.</strong><br><ul><li>Published on %2.<li>This model has %3 likes.<li>This model has %4 downloads.<li>More info can be found <a href="https://huggingface.co/%5">here.</a></ul> <strong>模型作者:%1</strong><br><ul><li>發佈日期:%2<li>累積讚數:%3 個讚<li>下載次數:%4 次<li>更多資訊請查閱<a href="https://huggingface.co/%5">此處</a>。</ul> @@ -1591,87 +2181,175 @@ model to get started 模型 - + + %1 system message? + + + + + + Clear + + + + + + Reset + + + + + The system message will be %1. + + + + + removed + + + + + + reset to the default + + + + + %1 chat template? + + + + + The chat template will be %1. + + + + + erased + + + + Model Settings 模型設定 - + Clone 複製 - + Remove 移除 - + Name 名稱 - + Model File 模型檔案 - System Prompt - 系統提示詞 + 系統提示詞 - Prefixed at the beginning of every conversation. Must contain the appropriate framing tokens. - 在每個對話的開頭加上前綴。必須包含適當的構建符元(framing tokens)。 + 在每個對話的開頭加上前綴。必須包含適當的構建符元(framing tokens)。 - Prompt Template - 提示詞模板 + 提示詞模板 - The template that wraps every prompt. - 包裝每個提示詞的模板。 + 包裝每個提示詞的模板。 - Must contain the string "%1" to be replaced with the user's input. - 必須包含要替換為使用者輸入的字串「%1」。 + 必須包含要替換為使用者輸入的字串「%1」。 + + + + System Message + + + + + A message to set the context or guide the behavior of the model. Leave blank for none. NOTE: Since GPT4All 3.5, this should not contain control tokens. + + + + + System message is not <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">plain text</a>. + + + + + Chat Template + + + + + This Jinja template turns the chat into input for the model. + + + + + No <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> configured. + + + + + The <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">chat template</a> cannot be blank. + + + + + <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Syntax error</a>: %1 + - + + Chat template is not in <a href="https://docs.gpt4all.io/gpt4all_desktop/chat_templates.html">Jinja format</a>. + + + + Chat Name Prompt 交談名稱提示詞 - + Prompt used to automatically generate chat names. 用於自動生成交談名稱的提示詞。 - + Suggested FollowUp Prompt 後續建議提示詞 - + Prompt used to generate suggested follow-up questions. 用於生成後續建議問題的提示詞。 - + Context Length 語境長度 - + Number of input and output tokens the model sees. 模型看見的輸入與輸出的符元數量。 - + Maximum combined prompt/response tokens before information is lost. Using more context than the model was trained on will yield poor results. NOTE: Does not take effect until you reload the model. @@ -1680,128 +2358,128 @@ NOTE: Does not take effect until you reload the model. 注意:重新載入模型後才會生效。 - + Temperature 語境溫度 - + Randomness of model output. Higher -> more variation. 模型輸出的隨機性。更高 -> 更多變化。 - + Temperature increases the chances of choosing less likely tokens. NOTE: Higher temperature gives more creative but less predictable outputs. 語境溫度會提高選擇不容易出現的符元機率。(Temperature) 注意:較高的語境溫度會生成更多創意,但輸出的可預測性會相對較差。 - + Top-P 核心採樣 - + Nucleus Sampling factor. Lower -> more predictable. 核心採樣因子。更低 -> 更可預測。 - + Only the most likely tokens up to a total probability of top_p can be chosen. NOTE: Prevents choosing highly unlikely tokens. 只選擇總機率約為核心採樣,最有可能性的符元。(Top-P) 注意:用於避免選擇不容易出現的符元。 - + Min-P 最小符元機率 - + Minimum token probability. Higher -> more predictable. 最小符元機率。更高 -> 更可預測。 - + Sets the minimum relative probability for a token to be considered. 設定要考慮的符元的最小相對機率。(Min-P) - + Top-K 高頻率採樣機率 - + Size of selection pool for tokens. 符元選擇池的大小。 - + Only the top K most likely tokens will be chosen from. 只選擇前 K 個最有可能性的符元。(Top-K) - + Max Length 最大長度 - + Maximum response length, in tokens. 最大響應長度(以符元為單位)。 - + Prompt Batch Size 提示詞批次大小 - + The batch size used for prompt processing. 用於即時處理的批量大小。 - + Amount of prompt tokens to process at once. NOTE: Higher values can speed up reading prompts but will use more RAM. 一次處理的提示詞符元數量。(Prompt Batch Size) 注意:較高的值可以加快讀取提示詞的速度,但會使用比較多的記憶體。 - + Repeat Penalty 重複處罰 - + Repetition penalty factor. Set to 1 to disable. 重複懲罰因子。設定為 1 以停用。 - + Repeat Penalty Tokens 重複懲罰符元 - + Number of previous tokens used for penalty. 之前用於懲罰的符元數量。 - + GPU Layers 圖形處理器負載層 - + Number of model layers to load into VRAM. 要載入到顯示記憶體中的模型層數。 - + How many model layers to load into VRAM. Decrease this if GPT4All runs out of VRAM while loading this model. Lower values increase CPU load and RAM usage, and make inference slower. NOTE: Does not take effect until you reload the model. @@ -2058,15 +2736,38 @@ NOTE: Does not take effect until you reload the model. 請選擇一個資料夾 + + MySettingsLabel + + + Clear + + + + + Reset + + + MySettingsTab - + + Restore defaults? + + + + + This page of settings will be reset to the defaults. + + + + Restore Defaults 恢復預設值 - + Restores settings dialog to a default state 恢復設定對話視窗到預設狀態 @@ -2263,6 +2964,11 @@ Nomic AI 將保留附加在您的資料上的所有署名訊息,並且您將 Describes what will happen when you opt-in 解釋當您加入計畫後,會發生什麼事情 + + + Opt-in to anonymous usage analytics used to improve GPT4All + + @@ -2307,6 +3013,11 @@ Nomic AI 將保留附加在您的資料上的所有署名訊息,並且您將 Allow opt-out for anonymous usage statistics 終止並退出匿名使用統計計畫 + + + Opt-in to anonymous sharing of chats to the GPT4All Datalake + + @@ -2337,25 +3048,20 @@ Nomic AI 將保留附加在您的資料上的所有署名訊息,並且您將 SwitchModelDialog - <b>Warning:</b> changing the model will erase the current conversation. Do you wish to continue? - <b>警告:</b> 變更模型將會清除目前對話內容。您真的想要繼續嗎? + <b>警告:</b> 變更模型將會清除目前對話內容。您真的想要繼續嗎? - Continue - 繼續 + 繼續 - Continue with model loading - 繼續載入模型 + 繼續載入模型 - - Cancel - 取消 + 取消 @@ -2394,125 +3100,135 @@ Nomic AI 將保留附加在您的資料上的所有署名訊息,並且您將 main - + GPT4All v%1 GPT4All v%1 - + + Restore + + + + + Quit + + + + <h3>Encountered an error starting up:</h3><br><i>"Incompatible hardware detected."</i><br><br>Unfortunately, your CPU does not meet the minimal requirements to run this program. In particular, it does not support AVX intrinsics which this program requires to successfully run a modern large language model. The only solution at this time is to upgrade your hardware to a more modern CPU.<br><br>See here for more information: <a href="https://en.wikipedia.org/wiki/Advanced_Vector_Extensions">https://en.wikipedia.org/wiki/Advanced_Vector_Extensions</a> <h3>啟動時發生錯誤:</h3><br><i>「偵測到不相容的硬體。」</i><br><br>糟糕!您的中央處理器不符合運行所需的最低需求。尤其,它不支援本程式運行現代大型語言模型所需的 AVX 指令集。目前唯一的解決方案,只有更新您的中央處理器及其相關硬體裝置。<br><br>更多資訊請查閱:<a href="https://zh.wikipedia.org/wiki/AVX指令集">AVX 指令集 - 維基百科</a> - + <h3>Encountered an error starting up:</h3><br><i>"Inability to access settings file."</i><br><br>Unfortunately, something is preventing the program from accessing the settings file. This could be caused by incorrect permissions in the local app config directory where the settings file is located. Check out our <a href="https://discord.gg/4M2QFmTt2k">discord channel</a> for help. <h3>啟動時發生錯誤:</h3><br><i>「無法存取設定檔。」</i><br><br>糟糕!有些東西正在阻止程式存取設定檔。這極為可能是由於設定檔所在的本機應用程式設定資料夾中的權限設定不正確所造成的。煩請洽詢我們的 <a href="https://discord.gg/4M2QFmTt2k">Discord 伺服器</a> 以尋求協助。 - + Connection to datalake failed. 連線資料湖泊失敗。 - + Saving chats. 儲存交談。 - + Network dialog 資料湖泊計畫對話視窗 - + opt-in to share feedback/conversations 分享回饋/對話計畫 - + Home view 首頁視圖 - + Home view of application 應用程式首頁視圖 - + Home 首頁 - + Chat view 查看交談 - + Chat view to interact with models 模型互動交談視圖 - + Chats 交談 - - + + Models 模型 - + Models view for installed models 已安裝模型的模型視圖 - - + + LocalDocs 我的文件 - + LocalDocs view to configure and use local docs 用於設定與使用我的文件的「我的文件」視圖 - - + + Settings 設定 - + Settings view for application configuration 應用程式設定視圖 - + The datalake is enabled 資料湖泊已啟用 - + Using a network model 使用一個網路模型 - + Server mode is enabled 伺服器模式已啟用 - + Installed models 已安裝的模型 - + View of installed models 已安裝的模型視圖