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
+
+
Website • Documentation • Discord • YouTube 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 .
-
-
-
## 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.
-
-
-
-## 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