Compare commits

..

44 Commits

Author SHA1 Message Date
Jared Van Bortel
be6347389e chat: cut v3.9.0 release (#3461)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-02-04 18:09:15 -05:00
Jared Van Bortel
8c10eccd24 changelog: fix missing credit
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-02-04 18:08:06 -05:00
ThiloteE
6ef0bd518e Whitelist OLMoE and Granite MoE (#3449)
Signed-off-by: ThiloteE <73715071+ThiloteE@users.noreply.github.com>
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
Co-authored-by: Jared Van Bortel <jared@nomic.ai>
2025-02-04 18:00:07 -05:00
Jared Van Bortel
04dc157b98 minja: update submodule to fix {# hang (redo) (#3457)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-02-04 17:30:04 -05:00
Jared Van Bortel
014bf67c63 Fix PDFium abuse that leads to a crash on Windows ARM (#3460)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-02-04 17:29:01 -05:00
Jared Van Bortel
8c9f26e249 Ignore DeepSeek-R1 "think" content in name/follow-up responses (#3458)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-02-04 12:08:17 -05:00
Andriy Mulyar
d4e6a6e485 Update README.md
Signed-off-by: Andriy Mulyar <andriy.mulyar@gmail.com>
2025-02-03 17:40:53 -05:00
Jared Van Bortel
a081255951 Revert "minja: update submodule to fix {# hang (#3446)"
This reverts commit c38c7455d8.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-02-03 12:44:27 -05:00
Jared Van Bortel
36c852b8be chat: work around Direct3D 11 rendering artifacts on win11 arm (#3450)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-02-03 11:47:40 -05:00
Jared Van Bortel
c38c7455d8 minja: update submodule to fix {# hang (#3446)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-02-03 11:25:21 -05:00
Jared Van Bortel
9131f4c432 Fix index used by LocalDocs when tool calling/thinking is active (#3451)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-02-03 11:22:46 -05:00
Jared Van Bortel
6bfa014594 cmake: remove reference to deleted README
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-31 16:26:17 -05:00
Jared Van Bortel
5af31278b7 ci: update to Qt 6.8.2 (#3442)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-31 11:20:50 -05:00
Jared Van Bortel
a80f023ed2 chat: release version 3.8.0 (#3439)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 20:06:42 -05:00
Jared Van Bortel
126042fdc9 remove ancient README
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 19:27:44 -05:00
Jared Van Bortel
1f2712d57c chat: fix emoji corruption (#3443)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 18:15:37 -05:00
Jared Van Bortel
f8f78c6677 ci: allow generate-config to run on tags
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 16:53:14 -05:00
Jared Van Bortel
643c733be3 ci: fix missing job_allow_tags
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 16:50:00 -05:00
Jared Van Bortel
0734694fb8 ci: remove conflicting pipeline.git.branch requirement
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 16:47:58 -05:00
Jared Van Bortel
e267512db9 chat: cut v3.8.0 release (#3441)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 16:37:02 -05:00
Jared Van Bortel
34037f3101 models: add DeepSeek-R1 distillations to official models list (#3437)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 16:23:41 -05:00
AT
007a7af1c8 Display DeepSeek-R1 thinking like Reasoner (#3440)
Signed-off-by: Adam Treat <treat.adam@gmail.com>
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
Co-authored-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 16:11:05 -05:00
Jared Van Bortel
f914ee56c9 chat: replace Jinja2Cpp with minja (#3433)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 16:01:49 -05:00
Jared Van Bortel
8a0ec5c303 ci: add missing signing holds to Windows ARM builds
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 15:23:18 -05:00
Jared Van Bortel
c2ee252ef2 chat: bump version to 3.8.0-dev0
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 13:12:47 -05:00
Jared Van Bortel
64dcf7682e ci: build offline installers when pipeline is scheduled (#3436)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-30 13:07:47 -05:00
AT
22b8278ef1 Don't block the gui thread for tool calls (#3435)
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2025-01-29 18:33:08 -05:00
Jared Van Bortel
adafa17c37 ci: verify that installers we build function and are signed (#3432)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-29 11:29:20 -05:00
Jared Van Bortel
343a4b6b6a Support DeepSeek-R1 Qwen (#3431)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-29 09:51:50 -05:00
Jared Van Bortel
6a8a840681 ci: selective signing and automatic release builds (#3430)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-28 17:41:01 -05:00
ThiloteE
88f5dac133 [Jinja] Fix typo in Phi-3.1-mini-128k-instruct replacement template (#3412)
Signed-off-by: ThiloteE <73715071+ThiloteE@users.noreply.github.com>
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
Co-authored-by: Jared Van Bortel <jared@nomic.ai>
2025-01-28 16:54:15 -05:00
Jared Van Bortel
0d974297a5 codeinterpreter: permit console.log with single string arg (#3426)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-27 15:22:20 -05:00
Jared Van Bortel
4fbc20ced9 cmake: do not modify gpt4all.app after signing it (#3417)
Signed-off-by: AT <manyoso@users.noreply.github.com>
2025-01-24 14:15:24 -05:00
Jared Van Bortel
f4f7de51e7 Revert "cmake: do not modify gpt4all.app after signing it (#3413)"
This reverts commit c01ac7fa93.

Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-24 13:21:34 -05:00
Jared Van Bortel
c01ac7fa93 cmake: do not modify gpt4all.app after signing it (#3413)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
Signed-off-by: AT <manyoso@users.noreply.github.com>
Co-authored-by: AT <manyoso@users.noreply.github.com>
2025-01-24 12:57:55 -05:00
Jared Van Bortel
173fdb18c2 Update to Qt 6.8.1 (#3386)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-24 10:29:59 -05:00
AT
8790586e57 Server view fix (#3411)
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2025-01-24 10:29:28 -05:00
AT
b98501c786 Fix regression while using localdocs with server API. (#3410)
Signed-off-by: Adam Treat <treat.adam@gmail.com>
2025-01-24 10:26:24 -05:00
Jared Van Bortel
49df6464a7 chat: bump version to v3.7.1-dev0
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-23 15:59:59 -05:00
Jared Van Bortel
6b719e99b5 metadata: fix typo
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-23 15:22:54 -05:00
Jared Van Bortel
d85fe40de8 chat: release version 3.7.0 (#3407)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-23 15:17:13 -05:00
Jared Van Bortel
15f66570fe ci: fix macOS codesigning (#3408)
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-23 11:41:34 -05:00
Jared Van Bortel
a97a28fe4f changelog: fix reference to wrong macOS version
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-22 13:09:01 -05:00
Jared Van Bortel
df2d124c19 changelog: add missing link
Signed-off-by: Jared Van Bortel <jared@nomic.ai>
2025-01-22 11:38:26 -05:00
44 changed files with 1535 additions and 807 deletions

View File

@@ -8,6 +8,10 @@ workflows:
generate-config:
jobs:
- path-filtering/filter:
filters:
tags:
only:
- /.*/
base-revision: main
config-path: .circleci/continue_config.yml
mapping: |

View File

@@ -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,19 +84,29 @@ 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 \
-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
mkdir upload
@@ -164,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:
@@ -187,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: |
@@ -211,19 +253,21 @@ 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 \
-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
mkdir upload
@@ -304,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:
@@ -340,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: |
@@ -353,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
@@ -382,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:
@@ -418,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: |
@@ -431,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
@@ -461,6 +536,14 @@ 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:
@@ -484,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: |
@@ -526,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 `
@@ -586,6 +673,12 @@ 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:
@@ -608,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: |
@@ -655,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 `
@@ -722,6 +819,13 @@ 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:
@@ -745,13 +849,13 @@ jobs:
- run:
name: Installing Qt
command: |
wget.exe "https://gpt4all.io/ci/qt-unified-windows-x64-4.6.0-online.exe"
# note: need x86_64 toolset for windeployqt.exe, which isn't provided with the ARM64 cross toolset
& .\qt-unified-windows-x64-4.6.0-online.exe --no-force-installations --no-default-installations `
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.651.win64_msvc2019_64 `
qt.qt6.651.win64_msvc2019_arm64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qthttpserver
--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: |
@@ -781,14 +885,14 @@ 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_arm64" `
"-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.5.1\msvc2019_arm64\lib\cmake\Qt6\qt.toolchain.cmake" `
"-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.5.1\msvc2019_64\bin\windeployqt.exe;--qtpaths;C:\Qt\6.5.1\msvc2019_arm64\bin\qtpaths.bat" `
"-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"
@@ -844,6 +948,12 @@ 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-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:
@@ -866,13 +976,13 @@ jobs:
- run:
name: Installing Qt
command: |
wget.exe "https://gpt4all.io/ci/qt-unified-windows-x64-4.6.0-online.exe"
# note: need x86_64 toolset for windeployqt.exe, which isn't provided with the ARM64 cross toolset
& .\qt-unified-windows-x64-4.6.0-online.exe --no-force-installations --no-default-installations `
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.651.win64_msvc2019_64 `
qt.qt6.651.win64_msvc2019_arm64 qt.qt6.651.qt5compat qt.qt6.651.debug_info qt.qt6.651.addons.qthttpserver
--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: |
@@ -907,14 +1017,14 @@ 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_arm64" `
"-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.5.1\msvc2019_arm64\lib\cmake\Qt6\qt.toolchain.cmake" `
"-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.5.1\msvc2019_64\bin\windeployqt.exe;--qtpaths;C:\Qt\6.5.1\msvc2019_arm64\bin\qtpaths.bat" `
"-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"
@@ -977,6 +1087,13 @@ 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-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:
@@ -1013,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 \
@@ -1061,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: |
@@ -1087,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 `
@@ -1123,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
@@ -1135,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 \
@@ -1169,7 +1299,7 @@ jobs:
cd gpt4all-bindings/typescript
npm run docs:build
build-py-docs:
deploy-docs:
docker:
- image: circleci/python:3.8
steps:
@@ -1373,7 +1503,7 @@ jobs:
paths:
- "*.whl"
publish-wheels:
deploy-wheels:
docker:
- image: circleci/python:3.8
steps:
@@ -1670,7 +1800,7 @@ jobs:
- prebuilds/win32-x64/*.node
- runtimes/win32-x64/*-*.dll
prepare-npm-pkg:
deploy-npm-pkg:
docker:
- image: cimg/base:stable
steps:
@@ -1726,11 +1856,27 @@ jobs:
npm set //registry.npmjs.org/:_authToken=$NPM_TOKEN
npm publish
main_only: &main_only
# 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:
@@ -1741,23 +1887,143 @@ 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
@@ -1766,77 +2032,69 @@ workflows:
- 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:
- hold
- 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:
<<: *main_only
- build-hold:
type: approval
- sign-hold:
type: approval
- build-online-chat-installer-macos:
<<: *main_only
context: gpt4all
requires:
- hold
- build-hold
- sign-online-chat-installer-macos:
<<: *main_only
context: gpt4all
requires:
- sign-hold
- build-online-chat-installer-macos
- notarize-online-chat-installer-macos:
<<: *main_only
context: gpt4all
requires:
- sign-online-chat-installer-macos
- build-online-chat-installer-windows:
<<: *main_only
context: gpt4all
requires:
- hold
- build-hold
- sign-online-chat-installer-windows:
<<: *main_only
context: gpt4all
requires:
- sign-hold
- build-online-chat-installer-windows
- build-online-chat-installer-windows-arm:
<<: *main_only
context: gpt4all
requires:
- hold
- build-hold
- sign-online-chat-installer-windows-arm:
<<: *main_only
context: gpt4all
requires:
- sign-hold
- build-online-chat-installer-windows-arm
- build-online-chat-installer-linux:
<<: *main_only
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
@@ -1854,23 +2112,25 @@ workflows:
- 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:
<<: *main_only
- build-py-docs:
<<: *main_only
- 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:
<<: *main_only
<<: *job_only_main
type: approval
- hold:
type: approval
@@ -1883,8 +2143,8 @@ workflows:
- build-py-windows:
requires:
- hold
- publish-wheels:
<<: *main_only
- deploy-wheels:
<<: *job_only_main
context: gpt4all
requires:
- pypi-hold
@@ -1893,36 +2153,29 @@ 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:
- backend-hold:
type: approval
- nodejs-hold:
type: approval
- npm-hold:
<<: *main_only
<<: *job_only_main
type: approval
- docs-hold:
type: approval
- build-bindings-backend-linux:
requires:
- hold
- backend-hold
- build-bindings-backend-macos:
requires:
- hold
- backend-hold
- build-bindings-backend-windows:
requires:
- hold
# NodeJs Jobs
- prepare-npm-pkg:
<<: *main_only
requires:
- npm-hold
- build-nodejs-linux
- build-nodejs-windows
- build-nodejs-macos
- backend-hold
- build-nodejs-linux:
requires:
- nodejs-hold
@@ -1935,3 +2188,13 @@ workflows:
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

12
.gitmodules vendored
View File

@@ -17,9 +17,9 @@
[submodule "gpt4all-chat/deps/QXlsx"]
path = gpt4all-chat/deps/QXlsx
url = https://github.com/nomic-ai/QXlsx.git
[submodule "gpt4all-chat/deps/Jinja2Cpp"]
path = gpt4all-chat/deps/Jinja2Cpp
url = https://github.com/nomic-ai/jinja2cpp.git
[submodule "gpt4all-chat/deps/rapidjson"]
path = gpt4all-chat/deps/rapidjson
url = https://github.com/nomic-ai/rapidjson.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

View File

@@ -1,5 +1,9 @@
<h1 align="center">GPT4All</h1>
<p align="center">
Now with support for DeepSeek R1 Distillations
</p>
<p align="center">
<a href="https://www.nomic.ai/gpt4all">Website</a> &bull; <a href="https://docs.gpt4all.io">Documentation</a> &bull; <a href="https://discord.gg/mGZE39AS3e">Discord</a> &bull; <a href="https://www.youtube.com/watch?v=gQcZDXRVJok">YouTube Tutorial</a>
</p>

View File

@@ -53,6 +53,7 @@ static const std::vector<const char *> KNOWN_ARCHES {
"gpt2",
// "gptj", -- no inference code
"gptneox",
"granitemoe",
"mpt",
"baichuan",
"starcoder",
@@ -80,6 +81,7 @@ static const std::vector<const char *> KNOWN_ARCHES {
"command-r",
// "dbrx", -- 16x12B parameters
"olmo",
"olmoe",
"openelm",
// "arctic", -- 10B+128x3.66B parameters
"deepseek2",

View File

@@ -4,11 +4,45 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [3.9.0] - 2025-02-04
### Added
- Whitelist OLMoE and Granite MoE model architectures (no Vulkan) (by [@ThiloteE](https://github.com/ThiloteE) in [#3449](https://github.com/nomic-ai/gpt4all/pull/3449))
### 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))
- Prevent DeepSeek-R1 reasoning from appearing in chat names and follow-up questions ([#3458](https://github.com/nomic-ai/gpt4all/pull/3458))
- Fix LocalDocs crash on Windows ARM when reading PDFs ([#3460](https://github.com/nomic-ai/gpt4all/pull/3460))
- Fix UI freeze when chat template is `{#` ([#3446](https://github.com/nomic-ai/gpt4all/pull/3446))
## [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))
@@ -17,7 +51,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- 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 Sonoma ([#3391](https://github.com/nomic-ai/gpt4all/pull/3391))
- 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
@@ -261,6 +295,9 @@ 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))
[3.9.0]: https://github.com/nomic-ai/gpt4all/compare/v3.8.0...v3.9.0
[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

View File

@@ -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 7)
set(APP_VERSION_MINOR 9)
set(APP_VERSION_PATCH 0)
set(APP_VERSION_BASE "${APP_VERSION_MAJOR}.${APP_VERSION_MINOR}.${APP_VERSION_PATCH}")
set(APP_VERSION "${APP_VERSION_BASE}")
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)
@@ -31,6 +35,8 @@ option(GPT4ALL_SIGN_INSTALL "Sign installed binaries and installers (requires si
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)
@@ -47,7 +53,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if (MSVC)
# Enable accurate __cplusplus macro to fix errors in Jinja2Cpp
# Enable accurate __cplusplus macro
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:/Zc:__cplusplus>)
endif()
@@ -86,12 +92,6 @@ 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)
set(GPT4ALL_QT_COMPONENTS Core HttpServer LinguistTools Quick QuickDialogs2 Sql Svg)
set(GPT4ALL_USING_QTPDF OFF)
@@ -126,10 +126,24 @@ message(STATUS "Qt 6 root directory: ${Qt6_ROOT_DIR}")
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
set(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}/install CACHE PATH "..." FORCE)
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)
@@ -437,7 +451,10 @@ else()
target_link_libraries(chat PRIVATE pdfium)
endif()
target_link_libraries(chat
PRIVATE llmodel SingleApplication fmt::fmt duckx::duckx QXlsx jinja2cpp)
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})
@@ -445,12 +462,18 @@ endif()
# -- install --
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
@@ -473,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)
@@ -503,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)
@@ -520,9 +543,9 @@ endif()
if (NOT GPT4ALL_USING_QTPDF)
# Install PDFium
if (WIN32)
install(FILES "${PDFium_LIBRARY}" DESTINATION bin COMPONENT ${COMPONENT_NAME_MAIN})
install(FILES ${PDFium_LIBRARY} DESTINATION bin COMPONENT ${COMPONENT_NAME_MAIN}) # .dll
else()
install(FILES "${PDFium_LIBRARY}" DESTINATION lib COMPONENT ${COMPONENT_NAME_MAIN})
install(FILES ${PDFium_LIBRARY} DESTINATION ${GPT4ALL_LIB_DEST} COMPONENT ${COMPONENT_NAME_MAIN}) # .so/.dylib
endif()
endif()

View File

@@ -1,45 +0,0 @@
# gpt4all-chat
Cross platform Qt based GUI for GPT4All versions with GPT-J as the base
model. NOTE: The model seen in the screenshot is actually a preview of a
new training run for GPT4All based on GPT-J. The GPT4All project is busy
at work getting ready to release this model including installers for all
three major OS's. In the meantime, you can try this UI out with the original
GPT-J model by following build instructions below.
![image](https://user-images.githubusercontent.com/50458173/231464085-da9edff6-a593-410e-8f38-7513f75c8aab.png)
## Install
One click installers for macOS, Linux, and Windows at https://www.nomic.ai/gpt4all
## Features
* Cross-platform (Linux, Windows, MacOSX)
* The UI is made to look and feel like you've come to expect from a chatty gpt
* Check for updates so you can always stay fresh with latest models
* Easy to install with precompiled binaries available for all three major desktop platforms
* Multi-modal - Ability to load more than one model and switch between them
* Multi-chat - a list of current and past chats and the ability to save/delete/export and switch between
* Supports models that are supported by llama.cpp
* Model downloader in GUI featuring many popular open source models
* Settings dialog to change temp, top_p, min_p, top_k, threads, etc
* Copy your conversation to clipboard
* RAG via LocalDocs feature
* Check for updates to get the very latest GUI
## Building and running
* Follow the visual instructions on the [build_and_run](build_and_run.md) page
## Getting the latest
If you've already checked out the source code and/or built the program make sure when you do a git fetch to get the latest changes and that you also do `git submodule update --init --recursive` to update the submodules. (If you ever run into trouble, deinitializing via `git submodule deinit -f .` and then initializing again via `git submodule update --init --recursive` fixes most issues)
## Contributing
* Pull requests welcome. See the feature wish list for ideas :)
## License
The source code of this chat interface is currently under a MIT license.

View File

@@ -1,6 +0,0 @@
#ifndef CONFIG_H
#define CONFIG_H
#define APP_VERSION "@APP_VERSION@"
#endif // CONFIG_H

View File

@@ -37,7 +37,6 @@ 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")

View File

@@ -8,12 +8,6 @@ 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"

View File

@@ -15,43 +15,34 @@ add_subdirectory(DuckX)
set(QT_VERSION_MAJOR 6)
add_subdirectory(QXlsx/QXlsx)
# forked dependency of Jinja2Cpp
set(RAPIDJSON_BUILD_DOC OFF)
set(RAPIDJSON_BUILD_EXAMPLES OFF)
set(RAPIDJSON_BUILD_TESTS OFF)
set(RAPIDJSON_ENABLE_INSTRUMENTATION_OPT OFF)
add_subdirectory(rapidjson)
add_subdirectory(Jinja2Cpp)
if (NOT GPT4ALL_USING_QTPDF)
# If we do not use QtPDF, we need to get PDFium.
set(GPT4ALL_PDFIUM_TAG "chromium/6954")
set(GPT4ALL_PDFIUM_TAG "chromium/6996")
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"
URL_HASH "SHA256=68b381b87efed539f2e33ae1e280304c9a42643a878cc296c1d66a93b0cb4335"
)
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"
URL_HASH "SHA256=83e714c302ceacccf403826d5cb57ea39b77f393d83b8d5781283012774a9378"
)
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"
URL_HASH "SHA256=78e77e871453a4915cbf66fb381b951c9932f88a747c6b2b33c9f27ec2371445"
)
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"
URL_HASH "SHA256=e7577f3242ff9c1df50025f9615673a43601a201bc51ee4792975f98920793a2"
)
endif()

View File

@@ -1,16 +1,16 @@
## Latest News
GPT4All v3.6.1 was released on December 20th and fixes issues with the stop generation and copy conversation buttons which were broken in v3.6.0.
GPT4All v3.8.0 was released on January 30th. Changes include:
---
GPT4All v3.6.0 was released on December 19th. Changes include:
* **Reasoner v1:**
* Built-in javascript code interpreter tool.
* Custom curated model that utilizes the code interpreter to break down, analyze, perform, and verify complex reasoning tasks.
* **Templates:** Automatically substitute chat templates that are not compatible with Jinja2Cpp in GGUFs.
* **Fixes:**
* Remote model template to allow for XML in messages.
* Jinja2Cpp bug that broke system message detection in chat templates.
* LocalDocs sources displaying in unconsolidated form after v3.5.0.
* **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.

View File

@@ -32,6 +32,66 @@
"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": "<p>The official Qwen2.5-Math-7B distillation of DeepSeek-R1.</p><ul><li>License: <a href=\"https://opensource.org/license/mit\">MIT</a></li><li>No restrictions on commercial use</li><li>#reasoning</li></ul>",
"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]*</think>', '') %}\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": "<p>The official Qwen2.5-14B distillation of DeepSeek-R1.</p><ul><li>License: <a href=\"https://opensource.org/license/mit\">MIT</a></li><li>No restrictions on commercial use</li><li>#reasoning</li></ul>",
"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]*</think>', '') %}\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": "<p>The official Llama-3.1-8B distillation of DeepSeek-R1.</p><ul><li>License: <a href=\"https://opensource.org/license/mit\">MIT</a></li><li>No restrictions on commercial use</li><li>#reasoning</li></ul>",
"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]*</think>', '') %}\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": "<p>The official Qwen2.5-Math-1.5B distillation of DeepSeek-R1.</p><ul><li>License: <a href=\"https://opensource.org/license/mit\">MIT</a></li><li>No restrictions on commercial use</li><li>#reasoning</li></ul>",
"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]*</think>', '') %}\n {{- '<Assistant>' + content + '<end▁of▁sentence>' }}\n {%- endif %}\n{%- endfor -%}\n{%- if add_generation_prompt %}\n {{- '<Assistant>' }}\n{%- endif %}"
},
{
"order": "b",
"md5sum": "27b44e8ae1817525164ddf4f8dae8af4",
@@ -472,7 +532,7 @@
"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",

View File

@@ -251,12 +251,22 @@
},
{
"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.",
"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.",
"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`)"
}
]

View File

@@ -13,6 +13,8 @@ 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
@@ -26,16 +28,20 @@ ColumnLayout {
anchors.bottom: parent.bottom
Item {
width: myTextArea.width
height: myTextArea.height
Layout.preferredWidth: myTextArea.implicitWidth
Layout.preferredHeight: myTextArea.implicitHeight
TextArea {
id: myTextArea
text: {
if (isError)
return qsTr("Analysis encountered error");
if (isCurrent)
return qsTr("Analyzing");
return qsTr("Analyzed");
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

View File

@@ -110,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
}
}
@@ -188,6 +189,17 @@ GridLayout {
isError: modelData.isToolCallError
}
}
DelegateChoice {
roleValue: "Think: ";
ChatCollapsibleItem {
Layout.fillWidth: true
textContent: modelData.content
isCurrent: modelData.isCurrentResponse
isError: false
isThinking: true
thinkingTime: modelData.thinkingTime
}
}
}
delegate: chooser

View File

@@ -828,7 +828,7 @@ Rectangle {
textInput.cursorPosition = text.length;
}
height: visible ? implicitHeight : 0
visible: name !== "ToolResponse: "
visible: name !== "ToolResponse: " && name !== "System: "
}
remove: Transition {

View File

@@ -181,6 +181,11 @@ QVariant Chat::popPrompt(int index)
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();
}
@@ -242,56 +247,71 @@ void Chat::responseStopped(qint64 promptResponseMs)
const QString possibleToolcall = m_chatModel->possibleToolcall();
ToolCallParser parser;
parser.update(possibleToolcall);
if (parser.state() == ToolEnums::ParseState::Complete) {
const QString toolCall = parser.toolCall();
// 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);
// The param is the code
const ToolParam param = { "code", ToolEnums::ParamType::String, code };
const QString result = toolInstance->run({param}, 10000 /*msecs to timeout*/);
const ToolEnums::Error error = toolInstance->error();
const QString errorString = toolInstance->errorString();
// Update the current response with meta information about toolcall and re-parent
m_chatModel->updateToolCall({
ToolCallConstants::CodeInterpreterFunction,
{ param },
result,
error,
errorString
});
++m_consecutiveToolCalls;
// We limit the number of consecutive toolcalls otherwise we get into a potentially endless loop
if (m_consecutiveToolCalls < 3 || error == ToolEnums::Error::NoError) {
resetResponseState();
emit promptRequested(m_collections); // triggers a new response
return;
}
}
if (m_generatedName.isEmpty())
emit generateNameRequested();
m_consecutiveToolCalls = 0;
Network::globalInstance()->trackChatEvent("response_complete", {
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::ThinkStartTag)
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;
}
@@ -361,11 +381,8 @@ void Chat::trySwitchContextOfLoadedModel()
void Chat::generatedNameChanged(const QString &name)
{
// Only use the first three words maximum and remove newlines and extra spaces
m_generatedName = name.simplified();
QStringList words = m_generatedName.split(' ', Qt::SkipEmptyParts);
int wordCount = qMin(7, words.size());
m_name = words.mid(0, wordCount).join(' ');
m_generatedName = name;
m_name = name;
emit nameChanged();
m_needsSave = true;
}

View File

@@ -55,7 +55,8 @@ public:
LocalDocsProcessing,
PromptProcessing,
GeneratingQuestions,
ResponseGeneration
ResponseGeneration,
ToolCallGeneration
};
Q_ENUM(ResponseState)
@@ -166,6 +167,9 @@ private Q_SLOTS:
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 handleModelLoadingError(const QString &error);

View File

@@ -12,12 +12,8 @@
#include "toolcallparser.h"
#include <fmt/format.h>
#include <jinja2cpp/error_info.h>
#include <jinja2cpp/template.h>
#include <jinja2cpp/template_env.h>
#include <jinja2cpp/user_callable.h>
#include <jinja2cpp/value.h>
#include <minja/minja.hpp>
#include <nlohmann/json.hpp>
#include <QDataStream>
#include <QDebug>
@@ -33,6 +29,7 @@
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QSet>
#include <QTextStream>
#include <QUrl>
#include <QWaitCondition>
#include <Qt>
@@ -60,44 +57,104 @@
using namespace Qt::Literals::StringLiterals;
using namespace ToolEnums;
namespace ranges = std::ranges;
using json = nlohmann::ordered_json;
//#define DEBUG
//#define DEBUG_MODEL_LOADING
// NOTE: not threadsafe
static jinja2::TemplateEnv *jinjaEnv()
static const std::shared_ptr<minja::Context> &jinjaEnv()
{
static std::optional<jinja2::TemplateEnv> environment;
static std::shared_ptr<minja::Context> environment;
if (!environment) {
auto &env = environment.emplace();
auto &settings = env.GetSettings();
settings.trimBlocks = true;
settings.lstripBlocks = true;
env.AddGlobal("raise_exception", jinja2::UserCallable(
/*callable*/ [](auto &params) -> jinja2::Value {
auto messageArg = params.args.find("message");
if (messageArg == params.args.end() || !messageArg->second.isString())
throw std::runtime_error("'message' argument to raise_exception() must be a string");
throw std::runtime_error(fmt::format("Jinja template error: {}", messageArg->second.asString()));
},
/*argsInfo*/ { jinja2::ArgInfo("message", /*isMandatory*/ true) }
));
env.AddGlobal("strftime_now", jinja2::UserCallable(
/*callable*/ [](auto &params) -> jinja2::Value {
environment = minja::Context::builtins();
environment->set("strftime_now", minja::simple_function(
"strftime_now", { "format" },
[](const std::shared_ptr<minja::Context> &, minja::Value &args) -> minja::Value {
auto format = args.at("format").get<std::string>();
using Clock = std::chrono::system_clock;
auto formatArg = params.args.find("format");
if (formatArg == params.args.end() || !formatArg->second.isString())
throw std::runtime_error("'format' argument to strftime_now() must be a string");
time_t nowUnix = Clock::to_time_t(Clock::now());
auto localDate = *std::localtime(&nowUnix);
std::ostringstream ss;
ss << std::put_time(&localDate, formatArg->second.asString().c_str());
ss << std::put_time(&localDate, format.c_str());
return ss.str();
},
/*argsInfo*/ { jinja2::ArgInfo("format", /*isMandatory*/ true) }
}
));
environment->set("regex_replace", minja::simple_function(
"regex_replace", { "str", "pattern", "repl" },
[](const std::shared_ptr<minja::Context> &, minja::Value &args) -> minja::Value {
auto str = args.at("str" ).get<std::string>();
auto pattern = args.at("pattern").get<std::string>();
auto repl = args.at("repl" ).get<std::string>();
return std::regex_replace(str, std::regex(pattern), repl);
}
));
}
return &*environment;
return environment;
}
class BaseResponseHandler {
public:
virtual void onSplitIntoTwo (const QString &startTag, const QString &firstBuffer, const QString &secondBuffer) = 0;
virtual void onSplitIntoThree (const QString &secondBuffer, const QString &thirdBuffer) = 0;
// "old-style" responses, with all of the implementation details left in
virtual void onOldResponseChunk(const QByteArray &chunk) = 0;
// notify of a "new-style" response that has been cleaned of tool calling
virtual bool onBufferResponse (const QString &response, int bufferIdx) = 0;
// notify of a "new-style" response, no tool calling applicable
virtual bool onRegularResponse () = 0;
virtual bool getStopGenerating () const = 0;
};
static auto promptModelWithTools(
LLModel *model, const LLModel::PromptCallback &promptCallback, BaseResponseHandler &respHandler,
const LLModel::PromptContext &ctx, const QByteArray &prompt, const QStringList &toolNames
) -> std::pair<QStringList, bool>
{
ToolCallParser toolCallParser(toolNames);
auto handleResponse = [&toolCallParser, &respHandler](LLModel::Token token, std::string_view piece) -> bool {
Q_UNUSED(token)
toolCallParser.update(piece.data());
// Split the response into two if needed
if (toolCallParser.numberOfBuffers() < 2 && toolCallParser.splitIfPossible()) {
const auto parseBuffers = toolCallParser.buffers();
Q_ASSERT(parseBuffers.size() == 2);
respHandler.onSplitIntoTwo(toolCallParser.startTag(), parseBuffers.at(0), parseBuffers.at(1));
}
// Split the response into three if needed
if (toolCallParser.numberOfBuffers() < 3 && toolCallParser.startTag() == ToolCallConstants::ThinkStartTag
&& toolCallParser.splitIfPossible()) {
const auto parseBuffers = toolCallParser.buffers();
Q_ASSERT(parseBuffers.size() == 3);
respHandler.onSplitIntoThree(parseBuffers.at(1), parseBuffers.at(2));
}
respHandler.onOldResponseChunk(QByteArray::fromRawData(piece.data(), piece.size()));
bool ok;
const auto parseBuffers = toolCallParser.buffers();
if (parseBuffers.size() > 1) {
ok = respHandler.onBufferResponse(parseBuffers.last(), parseBuffers.size() - 1);
} else {
ok = respHandler.onRegularResponse();
}
if (!ok)
return false;
const bool shouldExecuteToolCall = toolCallParser.state() == ToolEnums::ParseState::Complete
&& toolCallParser.startTag() != ToolCallConstants::ThinkStartTag;
return !shouldExecuteToolCall && !respHandler.getStopGenerating();
};
model->prompt(std::string_view(prompt), promptCallback, handleResponse, ctx);
const bool shouldExecuteToolCall = toolCallParser.state() == ToolEnums::ParseState::Complete
&& toolCallParser.startTag() != ToolCallConstants::ThinkStartTag;
return { toolCallParser.buffers(), shouldExecuteToolCall };
}
class LLModelStore {
@@ -738,7 +795,8 @@ std::vector<MessageItem> ChatLLM::forkConversation(const QString &prompt) const
conversation.reserve(items.size() + 1);
conversation.assign(items.begin(), items.end());
}
conversation.emplace_back(MessageItem::Type::Prompt, prompt.toUtf8());
qsizetype nextIndex = conversation.empty() ? 0 : conversation.back().index().value() + 1;
conversation.emplace_back(nextIndex, MessageItem::Type::Prompt, prompt.toUtf8());
return conversation;
}
@@ -757,19 +815,18 @@ static uint parseJinjaTemplateVersion(QStringView tmpl)
return 0;
}
static auto loadJinjaTemplate(
std::optional<jinja2::Template> &tmpl /*out*/, const std::string &source
) -> jinja2::Result<void>
static std::shared_ptr<minja::TemplateNode> loadJinjaTemplate(const std::string &source)
{
tmpl.emplace(jinjaEnv());
return tmpl->Load(source);
return minja::Parser::parse(source, { .trim_blocks = true, .lstrip_blocks = true, .keep_trailing_newline = false });
}
std::optional<std::string> ChatLLM::checkJinjaTemplateError(const std::string &source)
{
std::optional<jinja2::Template> tmpl;
if (auto res = loadJinjaTemplate(tmpl, source); !res)
return res.error().ToString();
try {
loadJinjaTemplate(source);
} catch (const std::runtime_error &e) {
return e.what();
}
return std::nullopt;
}
@@ -801,29 +858,29 @@ std::string ChatLLM::applyJinjaTemplate(std::span<const MessageItem> items) cons
uint version = parseJinjaTemplateVersion(chatTemplate);
auto makeMap = [version](const MessageItem &item) {
return jinja2::GenericMap([msg = std::make_shared<JinjaMessage>(version, item)] { return msg.get(); });
return JinjaMessage(version, item).AsJson();
};
std::unique_ptr<MessageItem> systemItem;
bool useSystem = !isAllSpace(systemMessage);
jinja2::ValuesList messages;
json::array_t messages;
messages.reserve(useSystem + items.size());
if (useSystem) {
systemItem = std::make_unique<MessageItem>(MessageItem::Type::System, systemMessage.toUtf8());
systemItem = std::make_unique<MessageItem>(MessageItem::system_tag, systemMessage.toUtf8());
messages.emplace_back(makeMap(*systemItem));
}
for (auto &item : items)
messages.emplace_back(makeMap(item));
jinja2::ValuesList toolList;
json::array_t toolList;
const int toolCount = ToolModel::globalInstance()->count();
for (int i = 0; i < toolCount; ++i) {
Tool *t = ToolModel::globalInstance()->get(i);
toolList.push_back(t->jinjaValue());
}
jinja2::ValuesMap params {
json::object_t params {
{ "messages", std::move(messages) },
{ "add_generation_prompt", true },
{ "toolList", toolList },
@@ -831,12 +888,14 @@ std::string ChatLLM::applyJinjaTemplate(std::span<const MessageItem> items) cons
for (auto &[name, token] : model->specialTokens())
params.emplace(std::move(name), std::move(token));
std::optional<jinja2::Template> tmpl;
auto maybeRendered = loadJinjaTemplate(tmpl, chatTemplate.toStdString())
.and_then([&] { return tmpl->RenderAsString(params); });
if (!maybeRendered)
throw std::runtime_error(fmt::format("Failed to parse chat template: {}", maybeRendered.error().ToString()));
return *maybeRendered;
try {
auto tmpl = loadJinjaTemplate(chatTemplate.toStdString());
auto context = minja::Context::make(minja::Value(std::move(params)), jinjaEnv());
return tmpl->render(context);
} catch (const std::runtime_error &e) {
throw std::runtime_error(fmt::format("Failed to parse chat template: {}", e.what()));
}
Q_UNREACHABLE();
}
auto ChatLLM::promptInternalChat(const QStringList &enabledCollections, const LLModel::PromptContext &ctx,
@@ -862,7 +921,7 @@ auto ChatLLM::promptInternalChat(const QStringList &enabledCollections, const LL
// Find the prompt that represents the query. Server chats are flexible and may not have one.
auto items = getChat();
if (auto peer = m_chatModel->getPeer(items, items.end() - 1)) // peer of response
query = { *peer - items.begin(), (*peer)->content() };
query = { (*peer)->index().value(), (*peer)->content() };
}
if (query) {
@@ -888,6 +947,62 @@ auto ChatLLM::promptInternalChat(const QStringList &enabledCollections, const LL
};
}
class ChatViewResponseHandler : public BaseResponseHandler {
public:
ChatViewResponseHandler(ChatLLM *cllm, QElapsedTimer *totalTime, ChatLLM::PromptResult *result)
: m_cllm(cllm), m_totalTime(totalTime), m_result(result) {}
void onSplitIntoTwo(const QString &startTag, const QString &firstBuffer, const QString &secondBuffer) override
{
if (startTag == ToolCallConstants::ThinkStartTag)
m_cllm->m_chatModel->splitThinking({ firstBuffer, secondBuffer });
else
m_cllm->m_chatModel->splitToolCall({ firstBuffer, secondBuffer });
}
void onSplitIntoThree(const QString &secondBuffer, const QString &thirdBuffer) override
{
m_cllm->m_chatModel->endThinking({ secondBuffer, thirdBuffer }, m_totalTime->elapsed());
}
void onOldResponseChunk(const QByteArray &chunk) override
{
m_result->responseTokens++;
m_cllm->m_timer->inc();
m_result->response.append(chunk);
}
bool onBufferResponse(const QString &response, int bufferIdx) override
{
Q_UNUSED(bufferIdx)
try {
m_cllm->m_chatModel->setResponseValue(response);
} catch (const std::exception &e) {
// We have a try/catch here because the main thread might have removed the response from
// the chatmodel by erasing the conversation during the response... the main thread sets
// m_stopGenerating before doing so, but it doesn't wait after that to reset the chatmodel
Q_ASSERT(m_cllm->m_stopGenerating);
return false;
}
emit m_cllm->responseChanged();
return true;
}
bool onRegularResponse() override
{
auto respStr = QString::fromUtf8(m_result->response);
return onBufferResponse(removeLeadingWhitespace(respStr), 0);
}
bool getStopGenerating() const override
{ return m_cllm->m_stopGenerating; }
private:
ChatLLM *m_cllm;
QElapsedTimer *m_totalTime;
ChatLLM::PromptResult *m_result;
};
auto ChatLLM::promptInternal(
const std::variant<std::span<const MessageItem>, std::string_view> &prompt,
const LLModel::PromptContext &ctx,
@@ -935,53 +1050,22 @@ auto ChatLLM::promptInternal(
return !m_stopGenerating;
};
ToolCallParser toolCallParser;
auto handleResponse = [this, &result, &toolCallParser](LLModel::Token token, std::string_view piece) -> bool {
Q_UNUSED(token)
result.responseTokens++;
m_timer->inc();
// FIXME: This is *not* necessarily fully formed utf data because it can be partial at this point
// handle this like below where we have a QByteArray
toolCallParser.update(QString::fromStdString(piece.data()));
// Create a toolcall and split the response if needed
if (!toolCallParser.hasSplit() && toolCallParser.state() == ToolEnums::ParseState::Partial) {
const QPair<QString, QString> pair = toolCallParser.split();
m_chatModel->splitToolCall(pair);
}
result.response.append(piece.data(), piece.size());
auto respStr = QString::fromUtf8(result.response);
try {
if (toolCallParser.hasSplit())
m_chatModel->setResponseValue(toolCallParser.buffer());
else
m_chatModel->setResponseValue(removeLeadingWhitespace(respStr));
} catch (const std::exception &e) {
// We have a try/catch here because the main thread might have removed the response from
// the chatmodel by erasing the conversation during the response... the main thread sets
// m_stopGenerating before doing so, but it doesn't wait after that to reset the chatmodel
Q_ASSERT(m_stopGenerating);
return false;
}
emit responseChanged();
const bool foundToolCall = toolCallParser.state() == ToolEnums::ParseState::Complete;
return !foundToolCall && !m_stopGenerating;
};
QElapsedTimer totalTime;
totalTime.start();
m_timer->start();
ChatViewResponseHandler respHandler(this, &totalTime, &result);
m_timer->start();
QStringList finalBuffers;
bool shouldExecuteTool;
try {
emit promptProcessing();
m_llModelInfo.model->setThreadCount(mySettings->threadCount());
m_stopGenerating = false;
m_llModelInfo.model->prompt(conversation, handlePrompt, handleResponse, ctx);
std::tie(finalBuffers, shouldExecuteTool) = promptModelWithTools(
m_llModelInfo.model.get(), handlePrompt, respHandler, ctx,
QByteArray::fromRawData(conversation.data(), conversation.size()),
ToolCallConstants::AllTagNames
);
} catch (...) {
m_timer->stop();
throw;
@@ -990,20 +1074,18 @@ auto ChatLLM::promptInternal(
m_timer->stop();
qint64 elapsed = totalTime.elapsed();
const bool foundToolCall = toolCallParser.state() == ToolEnums::ParseState::Complete;
// trim trailing whitespace
auto respStr = QString::fromUtf8(result.response);
if (!respStr.isEmpty() && (std::as_const(respStr).back().isSpace() || foundToolCall)) {
if (toolCallParser.hasSplit())
m_chatModel->setResponseValue(toolCallParser.buffer());
if (!respStr.isEmpty() && (std::as_const(respStr).back().isSpace() || finalBuffers.size() > 1)) {
if (finalBuffers.size() > 1)
m_chatModel->setResponseValue(finalBuffers.last());
else
m_chatModel->setResponseValue(respStr.trimmed());
emit responseChanged();
}
bool doQuestions = false;
if (!m_isServer && messageItems && !foundToolCall) {
if (!m_isServer && messageItems && !shouldExecuteTool) {
switch (mySettings->suggestionMode()) {
case SuggestionMode::On: doQuestions = true; break;
case SuggestionMode::LocalDocsOnly: doQuestions = usedLocalDocs; break;
@@ -1081,6 +1163,66 @@ void ChatLLM::reloadModel()
loadModel(m);
}
// This class throws discards the text within thinking tags, for use with chat names and follow-up questions.
class SimpleResponseHandler : public BaseResponseHandler {
public:
SimpleResponseHandler(ChatLLM *cllm)
: m_cllm(cllm) {}
void onSplitIntoTwo(const QString &startTag, const QString &firstBuffer, const QString &secondBuffer) override
{ /* no-op */ }
void onSplitIntoThree(const QString &secondBuffer, const QString &thirdBuffer) override
{ /* no-op */ }
void onOldResponseChunk(const QByteArray &chunk) override
{ m_response.append(chunk); }
bool onBufferResponse(const QString &response, int bufferIdx) override
{
if (bufferIdx == 1)
return true; // ignore "think" content
return onSimpleResponse(response);
}
bool onRegularResponse() override
{ return onBufferResponse(QString::fromUtf8(m_response), 0); }
bool getStopGenerating() const override
{ return m_cllm->m_stopGenerating; }
protected:
virtual bool onSimpleResponse(const QString &response) = 0;
protected:
ChatLLM *m_cllm;
QByteArray m_response;
};
class NameResponseHandler : public SimpleResponseHandler {
private:
// max length of chat names, in words
static constexpr qsizetype MAX_WORDS = 3;
public:
using SimpleResponseHandler::SimpleResponseHandler;
protected:
bool onSimpleResponse(const QString &response) override
{
QTextStream stream(const_cast<QString *>(&response), QIODeviceBase::ReadOnly);
QStringList words;
while (!stream.atEnd() && words.size() < MAX_WORDS) {
QString word;
stream >> word;
words << word;
}
emit m_cllm->generatedNameChanged(words.join(u' '));
return words.size() < MAX_WORDS || stream.atEnd();
}
};
void ChatLLM::generateName()
{
Q_ASSERT(isModelLoaded());
@@ -1097,23 +1239,15 @@ void ChatLLM::generateName()
return;
}
QByteArray response; // raw UTF-8
auto handleResponse = [this, &response](LLModel::Token token, std::string_view piece) -> bool {
Q_UNUSED(token)
response.append(piece.data(), piece.size());
QStringList words = QString::fromUtf8(response).simplified().split(u' ', Qt::SkipEmptyParts);
emit generatedNameChanged(words.join(u' '));
return words.size() <= 3;
};
NameResponseHandler respHandler(this);
try {
m_llModelInfo.model->prompt(
applyJinjaTemplate(forkConversation(chatNamePrompt)),
[this](auto &&...) { return !m_stopGenerating; },
handleResponse,
promptContextFromSettings(m_modelInfo)
promptModelWithTools(
m_llModelInfo.model.get(),
/*promptCallback*/ [this](auto &&...) { return !m_stopGenerating; },
respHandler, promptContextFromSettings(m_modelInfo),
applyJinjaTemplate(forkConversation(chatNamePrompt)).c_str(),
{ ToolCallConstants::ThinkTagName }
);
} catch (const std::exception &e) {
qWarning() << "ChatLLM failed to generate name:" << e.what();
@@ -1125,13 +1259,43 @@ void ChatLLM::handleChatIdChanged(const QString &id)
m_llmThread.setObjectName(id);
}
void ChatLLM::generateQuestions(qint64 elapsed)
{
class QuestionResponseHandler : public SimpleResponseHandler {
public:
using SimpleResponseHandler::SimpleResponseHandler;
protected:
bool onSimpleResponse(const QString &response) override
{
auto responseUtf8Bytes = response.toUtf8().slice(m_offset);
auto responseUtf8 = std::string(responseUtf8Bytes.begin(), responseUtf8Bytes.end());
// extract all questions from response
ptrdiff_t lastMatchEnd = -1;
auto it = std::sregex_iterator(responseUtf8.begin(), responseUtf8.end(), s_reQuestion);
auto end = std::sregex_iterator();
for (; it != end; ++it) {
auto pos = it->position();
auto len = it->length();
lastMatchEnd = pos + len;
emit m_cllm->generatedQuestionFinished(QString::fromUtf8(&responseUtf8[pos], len));
}
// remove processed input from buffer
if (lastMatchEnd != -1)
m_offset += lastMatchEnd;
return true;
}
private:
// FIXME: This only works with response by the model in english which is not ideal for a multi-language
// model.
// match whole question sentences
static const std::regex reQuestion(R"(\b(?:What|Where|How|Why|When|Who|Which|Whose|Whom)\b[^?]*\?)");
static inline const std::regex s_reQuestion { R"(\b(?:What|Where|How|Why|When|Who|Which|Whose|Whom)\b[^?]*\?)" };
qsizetype m_offset = 0;
};
void ChatLLM::generateQuestions(qint64 elapsed)
{
Q_ASSERT(isModelLoaded());
if (!isModelLoaded()) {
emit responseStopped(elapsed);
@@ -1149,39 +1313,17 @@ void ChatLLM::generateQuestions(qint64 elapsed)
emit generatingQuestions();
std::string response; // raw UTF-8
auto handleResponse = [this, &response](LLModel::Token token, std::string_view piece) -> bool {
Q_UNUSED(token)
// add token to buffer
response.append(piece);
// extract all questions from response
ptrdiff_t lastMatchEnd = -1;
auto it = std::sregex_iterator(response.begin(), response.end(), reQuestion);
auto end = std::sregex_iterator();
for (; it != end; ++it) {
auto pos = it->position();
auto len = it->length();
lastMatchEnd = pos + len;
emit generatedQuestionFinished(QString::fromUtf8(&response[pos], len));
}
// remove processed input from buffer
if (lastMatchEnd != -1)
response.erase(0, lastMatchEnd);
return true;
};
QuestionResponseHandler respHandler(this);
QElapsedTimer totalTime;
totalTime.start();
try {
m_llModelInfo.model->prompt(
applyJinjaTemplate(forkConversation(suggestedFollowUpPrompt)),
[this](auto &&...) { return !m_stopGenerating; },
handleResponse,
promptContextFromSettings(m_modelInfo)
promptModelWithTools(
m_llModelInfo.model.get(),
/*promptCallback*/ [this](auto &&...) { return !m_stopGenerating; },
respHandler, promptContextFromSettings(m_modelInfo),
applyJinjaTemplate(forkConversation(suggestedFollowUpPrompt)).c_str(),
{ ToolCallConstants::ThinkTagName }
);
} catch (const std::exception &e) {
qWarning() << "ChatLLM failed to generate follow-up questions:" << e.what();

View File

@@ -30,6 +30,7 @@
using namespace Qt::Literals::StringLiterals;
class ChatViewResponseHandler;
class QDataStream;
// NOTE: values serialized to disk, do not change or reuse
@@ -285,6 +286,8 @@ private:
bool m_isServer;
bool m_forceMetal;
bool m_reloadingToChangeVariant;
friend class ChatViewResponseHandler;
friend class SimpleResponseHandler;
};
#endif // CHATLLM_H

View File

@@ -41,6 +41,12 @@ void ChatItem::serializeText(QDataStream &stream, int version)
stream << value;
}
void ChatItem::serializeThink(QDataStream &stream, int version)
{
stream << value;
stream << thinkingTime;
}
void ChatItem::serializeSubItems(QDataStream &stream, int version)
{
stream << name;
@@ -50,6 +56,7 @@ void ChatItem::serializeSubItems(QDataStream &stream, int version)
case ToolCall: { serializeToolCall(stream, version); break; }
case ToolResponse: { serializeToolResponse(stream, version); break; }
case Text: { serializeText(stream, version); break; }
case Think: { serializeThink(stream, version); break; }
case System:
case Prompt:
throw std::invalid_argument(fmt::format("cannot serialize subitem type {}", int(typ)));
@@ -162,6 +169,13 @@ bool ChatItem::deserializeResponse(QDataStream &stream, int version)
return true;
}
bool ChatItem::deserializeThink(QDataStream &stream, int version)
{
stream >> value;
stream >> thinkingTime;
return true;
}
bool ChatItem::deserializeSubItems(QDataStream &stream, int version)
{
stream >> name;
@@ -177,6 +191,7 @@ bool ChatItem::deserializeSubItems(QDataStream &stream, int version)
case ToolCall: { deserializeToolCall(stream, version); break; }
case ToolResponse: { deserializeToolResponse(stream, version); break; }
case Text: { deserializeText(stream, version); break; }
case Think: { deserializeThink(stream, version); break; }
case System:
case Prompt:
throw std::invalid_argument(fmt::format("cannot serialize subitem type {}", int(typ)));

View File

@@ -94,11 +94,28 @@ class MessageItem
public:
enum class Type { System, Prompt, Response, ToolResponse };
MessageItem(Type type, QString content)
: m_type(type), m_content(std::move(content)) {}
struct system_tag_t { explicit system_tag_t() = default; };
static inline constexpr system_tag_t system_tag = system_tag_t{};
MessageItem(Type type, QString content, const QList<ResultInfo> &sources, const QList<PromptAttachment> &promptAttachments)
: m_type(type), m_content(std::move(content)), m_sources(sources), m_promptAttachments(promptAttachments) {}
MessageItem(qsizetype index, Type type, QString content)
: m_index(index), m_type(type), m_content(std::move(content))
{
Q_ASSERT(type != Type::System); // use system_tag constructor
}
// Construct a system message with no index, since they are never stored in the chat
MessageItem(system_tag_t, QString content)
: m_type(Type::System), m_content(std::move(content)) {}
MessageItem(qsizetype index, Type type, QString content, const QList<ResultInfo> &sources, const QList<PromptAttachment> &promptAttachments)
: m_index(index)
, m_type(type)
, m_content(std::move(content))
, m_sources(sources)
, m_promptAttachments(promptAttachments) {}
// index of the parent ChatItem (system, prompt, response) in its container
std::optional<qsizetype> index() const { return m_index; }
Type type() const { return m_type; }
const QString &content() const { return m_content; }
@@ -126,10 +143,11 @@ public:
}
private:
Type m_type;
QString m_content;
QList<ResultInfo> m_sources;
QList<PromptAttachment> m_promptAttachments;
std::optional<qsizetype> m_index;
Type m_type;
QString m_content;
QList<ResultInfo> m_sources;
QList<PromptAttachment> m_promptAttachments;
};
Q_DECLARE_METATYPE(MessageItem)
@@ -159,8 +177,11 @@ class ChatItem : public QObject
Q_PROPERTY(bool thumbsUpState MEMBER thumbsUpState )
Q_PROPERTY(bool thumbsDownState MEMBER thumbsDownState)
// thinking
Q_PROPERTY(int thinkingTime MEMBER thinkingTime NOTIFY thinkingTimeChanged)
public:
enum class Type { System, Prompt, Response, Text, ToolCall, ToolResponse };
enum class Type { System, Prompt, Response, Text, ToolCall, ToolResponse, Think };
// tags for constructing ChatItems
struct prompt_tag_t { explicit prompt_tag_t () = default; };
@@ -169,12 +190,14 @@ public:
struct text_tag_t { explicit text_tag_t () = default; };
struct tool_call_tag_t { explicit tool_call_tag_t () = default; };
struct tool_response_tag_t { explicit tool_response_tag_t() = default; };
struct think_tag_t { explicit think_tag_t () = default; };
static inline constexpr prompt_tag_t prompt_tag = prompt_tag_t {};
static inline constexpr response_tag_t response_tag = response_tag_t {};
static inline constexpr system_tag_t system_tag = system_tag_t {};
static inline constexpr text_tag_t text_tag = text_tag_t {};
static inline constexpr tool_call_tag_t tool_call_tag = tool_call_tag_t {};
static inline constexpr tool_response_tag_t tool_response_tag = tool_response_tag_t {};
static inline constexpr think_tag_t think_tag = think_tag_t {};
public:
ChatItem(QObject *parent)
@@ -220,6 +243,10 @@ public:
: ChatItem(parent)
{ this->name = u"ToolResponse: "_s; this->value = value; }
ChatItem(QObject *parent, think_tag_t, const QString &value)
: ChatItem(parent)
{ this->name = u"Think: "_s; this->value = value; }
Type type() const
{
if (name == u"System: "_s)
@@ -234,6 +261,8 @@ public:
return Type::ToolCall;
if (name == u"ToolResponse: "_s)
return Type::ToolResponse;
if (name == u"Think: "_s)
return Type::Think;
throw std::invalid_argument(fmt::format("Chat item has unknown label: {:?}", name));
}
@@ -254,7 +283,7 @@ public:
if (type() == Type::Response) {
// We parse if this contains any part of a partial toolcall
ToolCallParser parser;
parser.update(value);
parser.update(value.toUtf8());
// If no tool call is detected, return the original value
if (parser.startIndex() < 0)
@@ -265,9 +294,11 @@ public:
return beforeToolCall;
}
// For tool calls we only return content if it is the code interpreter
if (type() == Type::Think)
return thinkContent(value);
if (type() == Type::ToolCall)
return codeInterpreterContent(value);
return toolCallContent(value);
// We don't show any of content from the tool response in the GUI
if (type() == Type::ToolResponse)
@@ -276,10 +307,21 @@ public:
return value;
}
QString codeInterpreterContent(const QString &value) const
QString thinkContent(const QString &value) const
{
ToolCallParser parser;
parser.update(value);
parser.update(value.toUtf8());
// Extract the content
QString content = parser.toolCall();
content = content.trimmed();
return content;
}
QString toolCallContent(const QString &value) const
{
ToolCallParser parser;
parser.update(value.toUtf8());
// Extract the code
QString code = parser.toolCall();
@@ -357,6 +399,12 @@ public:
return toolCallInfo.error != ToolEnums::Error::NoError;
}
void setThinkingTime(int t)
{
thinkingTime = t;
emit thinkingTimeChanged();
}
// NB: Assumes response is not current.
static ChatItem *fromMessageInput(QObject *parent, const MessageInput &message)
{
@@ -369,7 +417,7 @@ public:
Q_UNREACHABLE();
}
MessageItem asMessageItem() const
MessageItem asMessageItem(qsizetype index) const
{
MessageItem::Type msgType;
switch (auto typ = type()) {
@@ -380,9 +428,10 @@ public:
case ToolResponse: msgType = MessageItem::Type::ToolResponse; break;
case Text:
case ToolCall:
case Think:
throw std::invalid_argument(fmt::format("cannot convert ChatItem type {} to message item", int(typ)));
}
return { msgType, flattenedContent(), sources, promptAttachments };
return { index, msgType, flattenedContent(), sources, promptAttachments };
}
static QList<ResultInfo> consolidateSources(const QList<ResultInfo> &sources);
@@ -391,6 +440,7 @@ public:
void serializeToolCall(QDataStream &stream, int version);
void serializeToolResponse(QDataStream &stream, int version);
void serializeText(QDataStream &stream, int version);
void serializeThink(QDataStream &stream, int version);
void serializeSubItems(QDataStream &stream, int version); // recursive
void serialize(QDataStream &stream, int version);
@@ -399,6 +449,7 @@ public:
bool deserializeToolCall(QDataStream &stream, int version);
bool deserializeToolResponse(QDataStream &stream, int version);
bool deserializeText(QDataStream &stream, int version);
bool deserializeThink(QDataStream &stream, int version);
bool deserializeSubItems(QDataStream &stream, int version); // recursive
bool deserialize(QDataStream &stream, int version);
@@ -406,6 +457,7 @@ Q_SIGNALS:
void contentChanged();
void isTooCallErrorChanged();
void isCurrentResponseChanged();
void thinkingTimeChanged();
public:
@@ -429,6 +481,9 @@ public:
bool stopped = false;
bool thumbsUpState = false;
bool thumbsDownState = false;
// thinking time in ms
int thinkingTime = 0;
};
class ChatModel : public QAbstractListModel
@@ -500,6 +555,7 @@ private:
return std::nullopt;
}
// FIXME(jared): this should really be done at the parent level, not the sub-item level
static std::optional<qsizetype> getPeerInternal(const MessageItem *arr, qsizetype size, qsizetype index)
{
qsizetype peer;
@@ -879,6 +935,70 @@ public:
if (changed) emit dataChanged(createIndex(index, 0), createIndex(index, 0), {NewResponseRole});
}
Q_INVOKABLE void splitThinking(const QPair<QString, QString> &split)
{
qsizetype index;
{
QMutexLocker locker(&m_mutex);
if (m_chatItems.isEmpty() || m_chatItems.cend()[-1]->type() != ChatItem::Type::Response)
throw std::logic_error("can only set thinking on a chat that ends with a response");
index = m_chatItems.count() - 1;
ChatItem *currentResponse = m_chatItems.back();
Q_ASSERT(currentResponse->isCurrentResponse);
// Create a new response container for any text and the thinking
ChatItem *newResponse = new ChatItem(this, ChatItem::response_tag);
// Add preceding text if any
if (!split.first.isEmpty()) {
ChatItem *textItem = new ChatItem(this, ChatItem::text_tag, split.first);
newResponse->subItems.push_back(textItem);
}
// Add the thinking item
Q_ASSERT(!split.second.isEmpty());
ChatItem *thinkingItem = new ChatItem(this, ChatItem::think_tag, split.second);
thinkingItem->isCurrentResponse = true;
newResponse->subItems.push_back(thinkingItem);
// Add new response and reset our value
currentResponse->subItems.push_back(newResponse);
currentResponse->value = QString();
}
emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ChildItemsRole, ContentRole});
}
Q_INVOKABLE void endThinking(const QPair<QString, QString> &split, int thinkingTime)
{
qsizetype index;
{
QMutexLocker locker(&m_mutex);
if (m_chatItems.isEmpty() || m_chatItems.cend()[-1]->type() != ChatItem::Type::Response)
throw std::logic_error("can only end thinking on a chat that ends with a response");
index = m_chatItems.count() - 1;
ChatItem *currentResponse = m_chatItems.back();
Q_ASSERT(currentResponse->isCurrentResponse);
ChatItem *subResponse = currentResponse->subItems.back();
Q_ASSERT(subResponse->type() == ChatItem::Type::Response);
Q_ASSERT(subResponse->isCurrentResponse);
subResponse->setCurrentResponse(false);
ChatItem *thinkingItem = subResponse->subItems.back();
Q_ASSERT(thinkingItem->type() == ChatItem::Type::Think);
thinkingItem->setCurrentResponse(false);
thinkingItem->setValue(split.first);
thinkingItem->setThinkingTime(thinkingTime);
currentResponse->setValue(split.second);
}
emit dataChanged(createIndex(index, 0), createIndex(index, 0), {ChildItemsRole, ContentRole});
}
Q_INVOKABLE void splitToolCall(const QPair<QString, QString> &split)
{
qsizetype index;
@@ -1013,10 +1133,12 @@ public:
// A flattened version of the chat item tree used by the backend and jinja
QMutexLocker locker(&m_mutex);
std::vector<MessageItem> chatItems;
for (const ChatItem *item : m_chatItems) {
chatItems.reserve(chatItems.size() + item->subItems.size() + 1);
ranges::copy(item->subItems | views::transform(&ChatItem::asMessageItem), std::back_inserter(chatItems));
chatItems.push_back(item->asMessageItem());
for (qsizetype i : views::iota(0, m_chatItems.size())) {
auto *parent = m_chatItems.at(i);
chatItems.reserve(chatItems.size() + parent->subItems.size() + 1);
ranges::copy(parent->subItems | views::transform([&](auto *s) { return s->asMessageItem(i); }),
std::back_inserter(chatItems));
chatItems.push_back(parent->asMessageItem(i));
}
return chatItems;
}

View File

@@ -7,8 +7,15 @@
using namespace Qt::Literals::StringLiterals;
CodeInterpreter::CodeInterpreter()
: Tool()
, m_error(ToolEnums::Error::NoError)
{
m_worker = new CodeInterpreterWorker;
connect(this, &CodeInterpreter::request, m_worker, &CodeInterpreterWorker::request, Qt::QueuedConnection);
}
QString CodeInterpreter::run(const QList<ToolParam> &params, qint64 timeout)
void CodeInterpreter::run(const QList<ToolParam> &params)
{
m_error = ToolEnums::Error::NoError;
m_errorString = QString();
@@ -18,27 +25,24 @@ QString CodeInterpreter::run(const QList<ToolParam> &params, qint64 timeout)
&& params.first().type == ToolEnums::ParamType::String);
const QString code = params.first().value.toString();
QThread workerThread;
CodeInterpreterWorker worker;
worker.moveToThread(&workerThread);
connect(&worker, &CodeInterpreterWorker::finished, &workerThread, &QThread::quit, Qt::DirectConnection);
connect(&workerThread, &QThread::started, [&worker, code]() {
worker.request(code);
connect(m_worker, &CodeInterpreterWorker::finished, [this, params] {
m_error = m_worker->error();
m_errorString = m_worker->errorString();
emit runComplete({
ToolCallConstants::CodeInterpreterFunction,
params,
m_worker->response(),
m_error,
m_errorString
});
});
workerThread.start();
bool timedOut = !workerThread.wait(timeout);
if (timedOut) {
worker.interrupt(timeout); // thread safe
m_error = ToolEnums::Error::TimeoutError;
}
workerThread.quit();
workerThread.wait();
if (!timedOut) {
m_error = worker.error();
m_errorString = worker.errorString();
}
return worker.response();
emit request(code);
}
bool CodeInterpreter::interrupt()
{
return m_worker->interrupt();
}
QList<ToolParamInfo> CodeInterpreter::parameters() const
@@ -89,20 +93,20 @@ QString CodeInterpreter::exampleReply() const
CodeInterpreterWorker::CodeInterpreterWorker()
: QObject(nullptr)
, m_engine(new QJSEngine(this))
{
}
moveToThread(&m_thread);
void CodeInterpreterWorker::request(const QString &code)
{
JavaScriptConsoleCapture consoleCapture;
QJSValue consoleInternalObject = m_engine.newQObject(&consoleCapture);
m_engine.globalObject().setProperty("console_internal", consoleInternalObject);
QJSValue consoleInternalObject = m_engine->newQObject(&m_consoleCapture);
m_engine->globalObject().setProperty("console_internal", consoleInternalObject);
// preprocess console.log args in JS since Q_INVOKE doesn't support varargs
auto consoleObject = m_engine.evaluate(uR"(
auto consoleObject = m_engine->evaluate(uR"(
class Console {
log(...args) {
if (args.length && typeof args[0] === 'string')
if (args.length == 0)
return;
if (args.length >= 2 && typeof args[0] === 'string')
throw new Error('console.log string formatting not supported');
let cat = '';
for (const arg of args) {
@@ -114,15 +118,28 @@ void CodeInterpreterWorker::request(const QString &code)
new Console();
)"_s);
m_engine.globalObject().setProperty("console", consoleObject);
m_engine->globalObject().setProperty("console", consoleObject);
m_thread.start();
}
const QJSValue result = m_engine.evaluate(code);
void CodeInterpreterWorker::reset()
{
m_response.clear();
m_error = ToolEnums::Error::NoError;
m_errorString.clear();
m_consoleCapture.output.clear();
m_engine->setInterrupted(false);
}
void CodeInterpreterWorker::request(const QString &code)
{
reset();
const QJSValue result = m_engine->evaluate(code);
QString resultString;
if (m_engine.isInterrupted()) {
resultString = QString("Error: code execution was timed out as it exceeded %1 ms. Code must be written to ensure execution does not timeout.").arg(m_timeout);
} else if (result.isError()) {
if (m_engine->isInterrupted()) {
resultString = QString("Error: code execution was interrupted or timed out.");
} else if (result.isError()) {
// NOTE: We purposely do not set the m_error or m_errorString for the code interpreter since
// we *want* the model to see the response has an error so it can hopefully correct itself. The
// error member variables are intended for tools that have error conditions that cannot be corrected.
@@ -143,9 +160,16 @@ void CodeInterpreterWorker::request(const QString &code)
}
if (resultString.isEmpty())
resultString = consoleCapture.output;
else if (!consoleCapture.output.isEmpty())
resultString += "\n" + consoleCapture.output;
resultString = m_consoleCapture.output;
else if (!m_consoleCapture.output.isEmpty())
resultString += "\n" + m_consoleCapture.output;
m_response = resultString;
emit finished();
}
bool CodeInterpreterWorker::interrupt()
{
m_error = ToolEnums::Error::TimeoutError;
m_engine->setInterrupted(true);
return true;
}

View File

@@ -8,6 +8,7 @@
#include <QObject>
#include <QString>
#include <QtGlobal>
#include <QThread>
class JavaScriptConsoleCapture : public QObject
{
@@ -39,32 +40,37 @@ public:
CodeInterpreterWorker();
virtual ~CodeInterpreterWorker() {}
void reset();
QString response() const { return m_response; }
void request(const QString &code);
void interrupt(qint64 timeout) { m_timeout = timeout; m_engine.setInterrupted(true); }
ToolEnums::Error error() const { return m_error; }
QString errorString() const { return m_errorString; }
bool interrupt();
public Q_SLOTS:
void request(const QString &code);
Q_SIGNALS:
void finished();
private:
qint64 m_timeout = 0;
QJSEngine m_engine;
QString m_response;
ToolEnums::Error m_error = ToolEnums::Error::NoError;
QString m_errorString;
QThread m_thread;
JavaScriptConsoleCapture m_consoleCapture;
QJSEngine *m_engine = nullptr;
};
class CodeInterpreter : public Tool
{
Q_OBJECT
public:
explicit CodeInterpreter() : Tool(), m_error(ToolEnums::Error::NoError) {}
explicit CodeInterpreter();
virtual ~CodeInterpreter() {}
QString run(const QList<ToolParam> &params, qint64 timeout = 2000) override;
void run(const QList<ToolParam> &params) override;
bool interrupt() override;
ToolEnums::Error error() const override { return m_error; }
QString errorString() const override { return m_errorString; }
@@ -77,9 +83,13 @@ public:
QString exampleCall() const override;
QString exampleReply() const override;
Q_SIGNALS:
void request(const QString &code);
private:
ToolEnums::Error m_error = ToolEnums::Error::NoError;
QString m_errorString;
CodeInterpreterWorker *m_worker;
};
#endif // CODEINTERPRETER_H

View File

@@ -0,0 +1,7 @@
#pragma once
#define APP_VERSION "@APP_VERSION@"
#define G4A_CONFIG(name) (1/G4A_CONFIG_##name == 1)
#define G4A_CONFIG_force_d3d12 @GPT4ALL_CONFIG_FORCE_D3D12@

View File

@@ -1209,7 +1209,6 @@ public:
FPDF_ClosePage(m_page);
if (m_doc)
FPDF_CloseDocument(m_doc);
FPDF_DestroyLibrary();
}
int page() const override { return m_currentPage; }
@@ -1224,7 +1223,7 @@ private:
return std::nullopt;
if (m_page)
FPDF_ClosePage(m_page);
FPDF_ClosePage(std::exchange(m_page, nullptr));
m_page = FPDF_LoadPage(m_doc, m_currentPage++);
if (!m_page)
throw std::runtime_error("Failed to load page.");

View File

@@ -7,111 +7,75 @@
#include <QString>
#include <QUrl>
#include <memory>
#include <iterator>
#include <map>
#include <ranges>
#include <vector>
using namespace std::literals::string_view_literals;
namespace views = std::views;
using json = nlohmann::ordered_json;
JinjaResultInfo::~JinjaResultInfo() = default;
const JinjaFieldMap<ResultInfo> JinjaResultInfo::s_fields = {
{ "collection", [](auto &s) { return s.collection.toStdString(); } },
{ "path", [](auto &s) { return s.path .toStdString(); } },
{ "file", [](auto &s) { return s.file .toStdString(); } },
{ "title", [](auto &s) { return s.title .toStdString(); } },
{ "author", [](auto &s) { return s.author .toStdString(); } },
{ "date", [](auto &s) { return s.date .toStdString(); } },
{ "text", [](auto &s) { return s.text .toStdString(); } },
{ "page", [](auto &s) { return s.page; } },
{ "file_uri", [](auto &s) { return s.fileUri() .toStdString(); } },
};
JinjaPromptAttachment::~JinjaPromptAttachment() = default;
const JinjaFieldMap<PromptAttachment> JinjaPromptAttachment::s_fields = {
{ "url", [](auto &s) { return s.url.toString() .toStdString(); } },
{ "file", [](auto &s) { return s.file() .toStdString(); } },
{ "processed_content", [](auto &s) { return s.processedContent().toStdString(); } },
};
std::vector<std::string> JinjaMessage::GetKeys() const
json::object_t JinjaResultInfo::AsJson() const
{
std::vector<std::string> result;
auto &keys = this->keys();
result.reserve(keys.size());
result.assign(keys.begin(), keys.end());
return result;
return {
{ "collection", m_source->collection.toStdString() },
{ "path", m_source->path .toStdString() },
{ "file", m_source->file .toStdString() },
{ "title", m_source->title .toStdString() },
{ "author", m_source->author .toStdString() },
{ "date", m_source->date .toStdString() },
{ "text", m_source->text .toStdString() },
{ "page", m_source->page },
{ "file_uri", m_source->fileUri() .toStdString() },
};
}
auto JinjaMessage::keys() const -> const std::unordered_set<std::string_view> &
json::object_t JinjaPromptAttachment::AsJson() const
{
static const std::unordered_set<std::string_view> baseKeys
{ "role", "content" };
static const std::unordered_set<std::string_view> userKeys
{ "role", "content", "sources", "prompt_attachments" };
switch (m_item->type()) {
using enum MessageItem::Type;
case System:
case Response:
case ToolResponse:
return baseKeys;
case Prompt:
return userKeys;
break;
}
Q_UNREACHABLE();
return {
{ "url", m_attachment->url.toString() .toStdString() },
{ "file", m_attachment->file() .toStdString() },
{ "processed_content", m_attachment->processedContent().toStdString() },
};
}
bool operator==(const JinjaMessage &a, const JinjaMessage &b)
json::object_t JinjaMessage::AsJson() const
{
if (a.m_item == b.m_item)
return true;
const auto &[ia, ib] = std::tie(*a.m_item, *b.m_item);
auto type = ia.type();
if (type != ib.type() || ia.content() != ib.content())
return false;
switch (type) {
using enum MessageItem::Type;
case System:
case Response:
case ToolResponse:
return true;
case Prompt:
return ia.sources() == ib.sources() && ia.promptAttachments() == ib.promptAttachments();
break;
}
Q_UNREACHABLE();
}
const JinjaFieldMap<JinjaMessage> JinjaMessage::s_fields = {
{ "role", [](auto &m) {
switch (m.item().type()) {
json::object_t obj;
{
json::string_t role;
switch (m_item->type()) {
using enum MessageItem::Type;
case System: return "system"sv;
case Prompt: return "user"sv;
case Response: return "assistant"sv;
case ToolResponse: return "tool"sv;
break;
case System: role = "system"; break;
case Prompt: role = "user"; break;
case Response: role = "assistant"; break;
case ToolResponse: role = "tool"; break;
}
Q_UNREACHABLE();
} },
{ "content", [](auto &m) {
if (m.version() == 0 && m.item().type() == MessageItem::Type::Prompt)
return m.item().bakedPrompt().toStdString();
return m.item().content().toStdString();
} },
{ "sources", [](auto &m) {
auto sources = m.item().sources() | views::transform([](auto &r) {
return jinja2::GenericMap([map = std::make_shared<JinjaResultInfo>(r)] { return map.get(); });
});
return jinja2::ValuesList(sources.begin(), sources.end());
} },
{ "prompt_attachments", [](auto &m) {
auto attachments = m.item().promptAttachments() | views::transform([](auto &pa) {
return jinja2::GenericMap([map = std::make_shared<JinjaPromptAttachment>(pa)] { return map.get(); });
});
return jinja2::ValuesList(attachments.begin(), attachments.end());
} },
};
obj.emplace_back("role", std::move(role));
}
{
QString content;
if (m_version == 0 && m_item->type() == MessageItem::Type::Prompt) {
content = m_item->bakedPrompt();
} else {
content = m_item->content();
}
obj.emplace_back("content", content.toStdString());
}
if (m_item->type() == MessageItem::Type::Prompt) {
{
auto sources = m_item->sources() | views::transform([](auto &r) {
return JinjaResultInfo(r).AsJson();
});
obj.emplace("sources", json::array_t(sources.begin(), sources.end()));
}
{
auto attachments = m_item->promptAttachments() | views::transform([](auto &pa) {
return JinjaPromptAttachment(pa).AsJson();
});
obj.emplace("prompt_attachments", json::array_t(attachments.begin(), attachments.end()));
}
}
return obj;
}

View File

@@ -3,47 +3,17 @@
#include "chatmodel.h"
#include "database.h"
#include <jinja2cpp/value.h>
#include <functional>
#include <ranges>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <nlohmann/json.hpp>
#include <QtGlobal>
namespace views = std::views;
using json = nlohmann::ordered_json;
template <typename T>
using JinjaFieldMap = std::unordered_map<std::string_view, std::function<jinja2::Value (const T &)>>;
template <typename Derived>
class JinjaComparable : public jinja2::IMapItemAccessor {
class JinjaHelper {
public:
JinjaComparable() = default;
bool IsEqual(const jinja2::IComparable &other) const override;
private:
Q_DISABLE_COPY_MOVE(JinjaComparable)
};
template <typename Derived>
class JinjaHelper : public JinjaComparable<Derived> {
public:
size_t GetSize() const override
{ return Derived::s_fields.size(); }
bool HasValue(const std::string &name) const override
{ return Derived::s_fields.contains(name); }
jinja2::Value GetValueByName(const std::string &name) const override;
std::vector<std::string> GetKeys() const override
{ auto keys = views::elements<0>(Derived::s_fields); return { keys.begin(), keys.end() }; }
json::object_t AsJson() const { return static_cast<const Derived *>(this)->AsJson(); }
};
class JinjaResultInfo : public JinjaHelper<JinjaResultInfo> {
@@ -51,18 +21,10 @@ public:
explicit JinjaResultInfo(const ResultInfo &source) noexcept
: m_source(&source) {}
~JinjaResultInfo() override;
const ResultInfo &value() const { return *m_source; }
friend bool operator==(const JinjaResultInfo &a, const JinjaResultInfo &b)
{ return a.m_source == b.m_source || *a.m_source == *b.m_source; }
json::object_t AsJson() const;
private:
static const JinjaFieldMap<ResultInfo> s_fields;
const ResultInfo *m_source;
friend class JinjaHelper<JinjaResultInfo>;
};
class JinjaPromptAttachment : public JinjaHelper<JinjaPromptAttachment> {
@@ -70,18 +32,10 @@ public:
explicit JinjaPromptAttachment(const PromptAttachment &attachment) noexcept
: m_attachment(&attachment) {}
~JinjaPromptAttachment() override;
const PromptAttachment &value() const { return *m_attachment; }
friend bool operator==(const JinjaPromptAttachment &a, const JinjaPromptAttachment &b)
{ return a.m_attachment == b.m_attachment || *a.m_attachment == *b.m_attachment; }
json::object_t AsJson() const;
private:
static const JinjaFieldMap<PromptAttachment> s_fields;
const PromptAttachment *m_attachment;
friend class JinjaHelper<JinjaPromptAttachment>;
};
class JinjaMessage : public JinjaHelper<JinjaMessage> {
@@ -89,28 +43,9 @@ public:
explicit JinjaMessage(uint version, const MessageItem &item) noexcept
: m_version(version), m_item(&item) {}
const JinjaMessage &value () const { return *this; }
uint version() const { return m_version; }
const MessageItem &item () const { return *m_item; }
size_t GetSize() const override { return keys().size(); }
bool HasValue(const std::string &name) const override { return keys().contains(name); }
jinja2::Value GetValueByName(const std::string &name) const override
{ return HasValue(name) ? JinjaHelper::GetValueByName(name) : jinja2::EmptyValue(); }
std::vector<std::string> GetKeys() const override;
json::object_t AsJson() const;
private:
auto keys() const -> const std::unordered_set<std::string_view> &;
private:
static const JinjaFieldMap<JinjaMessage> s_fields;
uint m_version;
uint m_version;
const MessageItem *m_item;
friend class JinjaHelper<JinjaMessage>;
friend bool operator==(const JinjaMessage &a, const JinjaMessage &b);
};
#include "jinja_helpers.inl"

View File

@@ -1,17 +0,0 @@
template <typename D>
bool JinjaComparable<D>::IsEqual(const jinja2::IComparable &other) const
{
if (auto *omsg = dynamic_cast<const D *>(&other))
return *static_cast<const D *>(this) == *omsg;
return false;
}
template <typename D>
jinja2::Value JinjaHelper<D>::GetValueByName(const std::string &name) const
{
if (auto it = D::s_fields.find(name); it != D::s_fields.end()) {
auto [_, func] = *it;
return func(static_cast<const D *>(this)->value());
}
return jinja2::EmptyValue();
}

View File

@@ -5,26 +5,9 @@
// This is a list of prompt templates known to GPT4All and their associated replacements which are automatically used
// instead when loading the chat template from GGUF. These exist for two primary reasons:
// - HuggingFace model authors make ugly chat templates because they do not expect the end user to see them;
// - and our Jinja2Cpp-based template parsing is not fully compatible with HuggingFace transformers and jinja2.
// Below is a list of known incompatibilities with the official HF jinja2 implementation. These are not all necessarily
// reflected in the below substitution list, and this cannot be an exhaustive list because there are a plethora of edge
// cases in template parsing in which jinja2 and Jinja2Cpp differ. These are differences that could be reasonably
// expected to affect chat templates that could be seen in the wild, or that cause a crash:
// - Jinja2Cpp crashes (in debug builds) if given the template `a[""(`
// - Jinja2Cpp does not support these jinja2 constructs:
// - `is not none`
// - list slicing, e.g. `messages[1:]`
// - the jinja2.ext.loopcontrols extension, which HF enables by default
// - a missing space after a quote in substitution (e.g. `{{ 'foo'}}`), which *has* been seen in the wild
// - GPT4All does not currently support these HuggingFace template features:
// - customized "tojson" filter (we provide the built-in Jinja2Cpp one)
// - the AssistantTracker extension
// - and chat templates occasionally use features we do not support. This is less true now that we use minja.
// The substitution list.
// For templates that apply to models listed in models3.json, these should be copied there as well for best
// compatibility with older versions of GPT4All.
const std::unordered_map<std::string_view, std::string_view> CHAT_TEMPLATE_SUBSTITUTIONS {
// calme-2.1-phi3.5-4b.Q6_K.gguf (reported by ThilotE on Discord), Phi-3.5-mini-instruct-Q4_0.gguf (nomic-ai/gpt4all#3345)
@@ -52,6 +35,30 @@ const std::unordered_map<std::string_view, std::string_view> CHAT_TEMPLATE_SUBST
{{- '<|assistant|>\n' }}
{%- else %}
{{- eos_token }}
{%- endif %})TEMPLATE",
},
// DeepSeek-R1-Distill-Qwen-7B-Q4_0.gguf
{
// original
R"TEMPLATE({% if not add_generation_prompt is defined %}{% set add_generation_prompt = false %}{% endif %}{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='') %}{%- for message in messages %}{%- if message['role'] == 'system' %}{% set ns.system_prompt = message['content'] %}{%- endif %}{%- endfor %}{{bos_token}}{{ns.system_prompt}}{%- for message in messages %}{%- if message['role'] == 'user' %}{%- set ns.is_tool = false -%}{{'<User>' + message['content']}}{%- endif %}{%- if message['role'] == 'assistant' and message['content'] is none %}{%- set ns.is_tool = false -%}{%- for tool in message['tool_calls']%}{%- if not ns.is_first %}{{'<Assistant><tool▁calls▁begin><tool▁call▁begin>' + tool['type'] + '<tool▁sep>' + tool['function']['name'] + '\n' + '```json' + '\n' + tool['function']['arguments'] + '\n' + '```' + '<tool▁call▁end>'}}{%- set ns.is_first = true -%}{%- else %}{{'\n' + '<tool▁call▁begin>' + tool['type'] + '<tool▁sep>' + tool['function']['name'] + '\n' + '```json' + '\n' + tool['function']['arguments'] + '\n' + '```' + '<tool▁call▁end>'}}{{'<tool▁calls▁end><end▁of▁sentence>'}}{%- endif %}{%- endfor %}{%- endif %}{%- if message['role'] == 'assistant' and message['content'] is not none %}{%- if ns.is_tool %}{{'<tool▁outputs▁end>' + message['content'] + '<end▁of▁sentence>'}}{%- set ns.is_tool = false -%}{%- else %}{% set content = message['content'] %}{% if '</think>' in content %}{% set content = content.split('</think>')[-1] %}{% endif %}{{'<Assistant>' + content + '<end▁of▁sentence>'}}{%- endif %}{%- endif %}{%- if message['role'] == 'tool' %}{%- set ns.is_tool = true -%}{%- if ns.is_output_first %}{{'<tool▁outputs▁begin><tool▁output▁begin>' + message['content'] + '<tool▁output▁end>'}}{%- set ns.is_output_first = false %}{%- else %}{{'\n<tool▁output▁begin>' + message['content'] + '<tool▁output▁end>'}}{%- endif %}{%- endif %}{%- endfor -%}{% if ns.is_tool %}{{'<tool▁outputs▁end>'}}{% endif %}{% if add_generation_prompt and not ns.is_tool %}{{'<Assistant>'}}{% endif %})TEMPLATE",
// replacement
R"TEMPLATE({%- if not add_generation_prompt is defined %}
{%- set add_generation_prompt = false %}
{%- endif %}
{%- if messages[0]['role'] == 'system' %}
{{- messages[0]['content'] }}
{%- endif %}
{%- for message in messages %}
{%- if message['role'] == 'user' %}
{{- '<User>' + message['content'] }}
{%- endif %}
{%- if message['role'] == 'assistant' %}
{%- set content = message['content'] | regex_replace('^[\\s\\S]*</think>', '') %}
{{- '<Assistant>' + content + '<endofsentence>' }}
{%- endif %}
{%- endfor -%}
{%- if add_generation_prompt %}
{{- '<Assistant>' }}
{%- endif %})TEMPLATE",
},
// gemma-2-9b-it-Q4_0.gguf (nomic-ai/gpt4all#3282)
@@ -626,7 +633,7 @@ const std::unordered_map<std::string_view, std::string_view> CHAT_TEMPLATE_SUBST
// replacement
R"TEMPLATE({%- for message in messages %}
{%- if message['role'] == 'system' %}
{{-'<|system|>\n' + message['content'] + '<|end|>\n'}}
{{- '<|system|>\n' + message['content'] + '<|end|>\n' }}
{%- elif message['role'] == 'user' %}
{{- '<|user|>\n' + message['content'] + '<|end|>\n' }}
{%- elif message['role'] == 'assistant' %}

View File

@@ -25,6 +25,10 @@
#include <QVariant>
#include <Qt>
#if G4A_CONFIG(force_d3d12)
# include <QSGRendererInterface>
#endif
#ifndef GPT4ALL_USE_QTPDF
# include <fpdfview.h>
#endif
@@ -83,24 +87,27 @@ int main(int argc, char *argv[])
return 0;
}
#if G4A_CONFIG(force_d3d12)
QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D12);
#endif
#ifdef Q_OS_LINUX
app.setWindowIcon(QIcon(":/gpt4all/icons/gpt4all.svg"));
#endif
// set search path before constructing the MySettings instance, which relies on this
QString llmodelSearchPaths = QCoreApplication::applicationDirPath();
const QString libDir = QCoreApplication::applicationDirPath() + "/../lib/";
if (LLM::directoryExists(libDir))
llmodelSearchPaths += ";" + libDir;
#if defined(Q_OS_MAC)
const QString binDir = QCoreApplication::applicationDirPath() + "/../../../";
if (LLM::directoryExists(binDir))
llmodelSearchPaths += ";" + binDir;
const QString frameworksDir = QCoreApplication::applicationDirPath() + "/../Frameworks/";
if (LLM::directoryExists(frameworksDir))
llmodelSearchPaths += ";" + frameworksDir;
{
auto appDirPath = QCoreApplication::applicationDirPath();
QStringList searchPaths {
#ifdef Q_OS_DARWIN
u"%1/../Frameworks"_s.arg(appDirPath),
#else
appDirPath,
u"%1/../lib"_s.arg(appDirPath),
#endif
LLModel::Implementation::setImplementationsSearchPath(llmodelSearchPaths.toStdString());
};
LLModel::Implementation::setImplementationsSearchPath(searchPaths.join(u';').toStdString());
}
// Set the local and language translation before the qml engine has even been started. This will
// use the default system locale unless the user has explicitly set it to use a different one.
@@ -174,5 +181,9 @@ int main(int argc, char *argv[])
// Otherwise, we can get a heap-use-after-free inside of llama.cpp.
ChatListModel::globalInstance()->destroyChats();
#ifndef GPT4ALL_USE_QTPDF
FPDF_DestroyLibrary();
#endif
return res;
}

View File

@@ -1621,7 +1621,6 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
QString requiresVersion = obj["requires"].toString();
QString versionRemoved = obj["removedIn"].toString();
QString url = obj["url"].toString();
QByteArray modelHash = obj["md5sum"].toString().toLatin1();
bool isDefault = obj.contains("isDefault") && obj["isDefault"] == u"true"_s;
bool disableGUI = obj.contains("disableGUI") && obj["disableGUI"] == u"true"_s;
QString description = obj["description"].toString();
@@ -1632,6 +1631,16 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
QString type = obj["type"].toString();
bool isEmbeddingModel = obj["embeddingModel"].toBool();
QByteArray modelHash;
ModelInfo::HashAlgorithm hashAlgorithm;
if (auto it = obj.find("sha256sum"_L1); it != obj.end()) {
modelHash = it->toString().toLatin1();
hashAlgorithm = ModelInfo::Sha256;
} else {
modelHash = obj["md5sum"].toString().toLatin1();
hashAlgorithm = ModelInfo::Md5;
}
// Some models aren't supported in the GUI at all
if (disableGUI)
continue;
@@ -1660,7 +1669,7 @@ void ModelList::parseModelsJsonFile(const QByteArray &jsonData, bool save)
{ ModelList::FilenameRole, modelFilename },
{ ModelList::FilesizeRole, modelFilesize },
{ ModelList::HashRole, modelHash },
{ ModelList::HashAlgorithmRole, ModelInfo::Md5 },
{ ModelList::HashAlgorithmRole, hashAlgorithm },
{ ModelList::DefaultRole, isDefault },
{ ModelList::DescriptionRole, description },
{ ModelList::RequiresVersionRole, requiresVersion },

View File

@@ -269,7 +269,7 @@ private:
std::optional<QString> m_chatTemplate;
mutable std::optional<QString> m_modelChatTemplate;
QString m_systemMessage;
QString m_chatNamePrompt = "Describe the above conversation in seven words or less.";
QString m_chatNamePrompt = "Describe the above conversation. Your entire response must be three words or less.";
QString m_suggestedFollowUpPrompt = "Suggest three very short factual follow-up questions that have not been answered yet or cannot be found inspired by the previous conversation and excerpts.";
friend class MySettings;
friend class ModelList;

View File

@@ -1,12 +1,13 @@
#include "tool.h"
#include <jinja2cpp/value.h>
#include <string>
jinja2::Value Tool::jinjaValue() const
using json = nlohmann::ordered_json;
json::object_t Tool::jinjaValue() const
{
jinja2::ValuesList paramList;
json::array_t paramList;
const QList<ToolParamInfo> p = parameters();
for (auto &info : p) {
std::string typeStr;
@@ -20,26 +21,24 @@ jinja2::Value Tool::jinjaValue() const
case Boolean: typeStr = "boolean"; break;
case Null: typeStr = "null"; break;
}
jinja2::ValuesMap infoMap {
{ "name", info.name.toStdString() },
{ "type", typeStr},
paramList.emplace_back(json::initializer_list_t {
{ "name", info.name.toStdString() },
{ "type", typeStr },
{ "description", info.description.toStdString() },
{ "required", info.required }
};
paramList.push_back(infoMap);
{ "required", info.required },
});
}
jinja2::ValuesMap params {
{ "name", name().toStdString() },
{ "description", description().toStdString() },
{ "function", function().toStdString() },
{ "parameters", paramList },
return {
{ "name", name().toStdString() },
{ "description", description().toStdString() },
{ "function", function().toStdString() },
{ "parameters", paramList },
{ "symbolicFormat", symbolicFormat().toStdString() },
{ "examplePrompt", examplePrompt().toStdString() },
{ "exampleCall", exampleCall().toStdString() },
{ "exampleReply", exampleReply().toStdString() }
{ "examplePrompt", examplePrompt().toStdString() },
{ "exampleCall", exampleCall().toStdString() },
{ "exampleReply", exampleReply().toStdString() },
};
return params;
}
void ToolCallInfo::serialize(QDataStream &stream, int version)

View File

@@ -1,13 +1,16 @@
#ifndef TOOL_H
#define TOOL_H
#include <nlohmann/json.hpp>
#include <QList>
#include <QObject>
#include <QString>
#include <QVariant>
#include <QtGlobal>
#include <jinja2cpp/value.h>
using json = nlohmann::ordered_json;
namespace ToolEnums
{
@@ -25,6 +28,7 @@ namespace ToolEnums
enum class ParseState {
None,
InTagChoice,
InStart,
Partial,
Complete,
@@ -87,7 +91,8 @@ public:
Tool() : QObject(nullptr) {}
virtual ~Tool() {}
virtual QString run(const QList<ToolParam> &params, qint64 timeout = 2000) = 0;
virtual void run(const QList<ToolParam> &params) = 0;
virtual bool interrupt() = 0;
// Tools should set these if they encounter errors. For instance, a tool depending upon the network
// might set these error variables if the network is not available.
@@ -121,7 +126,10 @@ public:
bool operator==(const Tool &other) const { return function() == other.function(); }
jinja2::Value jinjaValue() const;
json::object_t jinjaValue() const;
Q_SIGNALS:
void runComplete(const ToolCallInfo &info);
};
#endif // TOOL_H

View File

@@ -1,16 +1,29 @@
#include "toolcallparser.h"
#include <QDebug>
#include "tool.h"
#include <QSet>
#include <QtGlobal>
#include <QtLogging>
#include <cstddef>
#include <stdexcept>
static const QString ToolCallStart = ToolCallConstants::CodeInterpreterTag;
static const QString ToolCallEnd = ToolCallConstants::CodeInterpreterEndTag;
ToolCallParser::ToolCallParser()
: ToolCallParser(ToolCallConstants::AllTagNames)
{}
ToolCallParser::ToolCallParser(const QStringList &tagNames)
{
QSet<QChar> firstChars;
for (auto &name : tagNames) {
if (name.isEmpty())
throw std::invalid_argument("ToolCallParser(): tag names must not be empty");
if (firstChars.contains(name.at(0)))
throw std::invalid_argument("ToolCallParser(): tag names must not share any prefix");
firstChars << name.at(0);
m_possibleStartTags << makeStartTag(name).toUtf8();
m_possibleEndTags << makeEndTag (name).toUtf8();
}
reset();
}
@@ -20,36 +33,68 @@ void ToolCallParser::reset()
resetSearchState();
// These are global states maintained between update calls
m_buffer.clear();
m_hasSplit = false;
m_buffers.clear();
m_buffers << QByteArray();
}
void ToolCallParser::resetSearchState()
{
m_expected = ToolCallStart.at(0);
m_expected = {'<'};
m_expectedIndex = 0;
m_state = ToolEnums::ParseState::None;
m_toolCall.clear();
m_startTagBuffer.clear();
m_endTagBuffer.clear();
m_currentTagIndex = -1;
m_startIndex = -1;
m_endIndex = -1;
}
bool ToolCallParser::isExpected(char c) const
{
return m_expected.isEmpty() || m_expected.contains(c);
}
void ToolCallParser::setExpected(const QList<QByteArray> &tags)
{
m_expected.clear();
for (const auto &tag : tags) {
Q_ASSERT(tag.size() > m_expectedIndex);
m_expected << tag.at(m_expectedIndex);
}
}
QByteArray ToolCallParser::startTag() const
{
if (m_currentTagIndex < 0)
return {};
return m_possibleStartTags.at(m_currentTagIndex);
}
QByteArray ToolCallParser::endTag() const
{
if (m_currentTagIndex < 0)
return {};
return m_possibleEndTags.at(m_currentTagIndex);
}
QByteArray &ToolCallParser::currentBuffer()
{
return m_buffers.last();
}
// This method is called with an arbitrary string and a current state. This method should take the
// current state into account and then parse through the update character by character to arrive at
// the new state.
void ToolCallParser::update(const QString &update)
void ToolCallParser::update(const QByteArray &update)
{
Q_ASSERT(m_state != ToolEnums::ParseState::Complete);
if (m_state == ToolEnums::ParseState::Complete) {
qWarning() << "ERROR: ToolCallParser::update already found a complete toolcall!";
return;
}
currentBuffer().append(update);
m_buffer.append(update);
for (size_t i = m_buffer.size() - update.size(); i < m_buffer.size(); ++i) {
const QChar c = m_buffer[i];
const bool foundMatch = m_expected.isNull() || c == m_expected;
for (qsizetype i = currentBuffer().size() - update.size(); i < currentBuffer().size(); ++i) {
const char c = currentBuffer()[i];
const bool foundMatch = isExpected(c);
if (!foundMatch) {
resetSearchState();
continue;
@@ -59,34 +104,58 @@ void ToolCallParser::update(const QString &update)
case ToolEnums::ParseState::None:
{
m_expectedIndex = 1;
m_expected = ToolCallStart.at(1);
m_state = ToolEnums::ParseState::InStart;
setExpected(m_possibleStartTags);
m_state = ToolEnums::ParseState::InTagChoice;
m_startIndex = i;
break;
}
case ToolEnums::ParseState::InTagChoice:
{
for (int i = 0; i < m_possibleStartTags.size(); ++i) {
const auto &tag = m_possibleStartTags.at(i);
if (c == tag.at(1)) m_currentTagIndex = i;
}
if (m_currentTagIndex >= 0) {
m_expectedIndex = 2;
setExpected({m_possibleStartTags.at(m_currentTagIndex)});
m_state = ToolEnums::ParseState::InStart;
} else
resetSearchState();
break;
}
case ToolEnums::ParseState::InStart:
{
if (m_expectedIndex == ToolCallStart.size() - 1) {
m_startTagBuffer.append(c);
const auto startTag = this->startTag();
Q_ASSERT(!startTag.isEmpty());
if (m_expectedIndex == startTag.size() - 1) {
m_expectedIndex = 0;
m_expected = QChar();
setExpected({});
m_state = ToolEnums::ParseState::Partial;
} else {
++m_expectedIndex;
m_expected = ToolCallStart.at(m_expectedIndex);
Q_ASSERT(m_currentTagIndex >= 0);
setExpected({startTag});
}
break;
}
case ToolEnums::ParseState::Partial:
{
Q_ASSERT(m_currentTagIndex >= 0);
const auto endTag = this->endTag();
Q_ASSERT(!endTag.isEmpty());
m_toolCall.append(c);
m_endTagBuffer.append(c);
if (m_endTagBuffer.size() > ToolCallEnd.size())
if (m_endTagBuffer.size() > endTag.size())
m_endTagBuffer.remove(0, 1);
if (m_endTagBuffer == ToolCallEnd) {
m_toolCall.chop(ToolCallEnd.size());
if (m_endTagBuffer == endTag) {
m_endIndex = i + 1;
m_toolCall.chop(endTag.size());
m_state = ToolEnums::ParseState::Complete;
m_endTagBuffer.clear();
}
break;
}
case ToolEnums::ParseState::Complete:
{
@@ -97,15 +166,35 @@ void ToolCallParser::update(const QString &update)
}
}
QPair<QString, QString> ToolCallParser::split()
bool ToolCallParser::splitIfPossible()
{
Q_ASSERT(m_state == ToolEnums::ParseState::Partial
|| m_state == ToolEnums::ParseState::Complete);
// The first split happens when we're in a partial state
if (m_buffers.size() < 2 && m_state == ToolEnums::ParseState::Partial) {
Q_ASSERT(m_startIndex >= 0);
const auto beforeToolCall = currentBuffer().left(m_startIndex);
const auto toolCall = currentBuffer().mid (m_startIndex);
m_buffers = { beforeToolCall, toolCall };
return true;
}
Q_ASSERT(m_startIndex >= 0);
m_hasSplit = true;
const QString beforeToolCall = m_buffer.left(m_startIndex);
m_buffer = m_buffer.mid(m_startIndex);
m_startIndex = 0;
return { beforeToolCall, m_buffer };
// The second split happens when we're in the complete state
if (m_buffers.size() < 3 && m_state == ToolEnums::ParseState::Complete) {
Q_ASSERT(m_endIndex >= 0);
const auto &beforeToolCall = m_buffers.first();
const auto toolCall = currentBuffer().left(m_endIndex);
const auto afterToolCall = currentBuffer().mid (m_endIndex);
m_buffers = { beforeToolCall, toolCall, afterToolCall };
return true;
}
return false;
}
QStringList ToolCallParser::buffers() const
{
QStringList result;
result.reserve(m_buffers.size());
for (const auto &buffer : m_buffers)
result << QString::fromUtf8(buffer);
return result;
}

View File

@@ -1,47 +1,73 @@
#ifndef TOOLCALLPARSER_H
#define TOOLCALLPARSER_H
#include "tool.h"
#include <QChar>
#include <QByteArray>
#include <QList>
#include <QString>
#include <QPair>
#include <QStringList>
namespace ToolEnums { enum class ParseState; }
using namespace Qt::Literals::StringLiterals;
namespace ToolCallConstants
{
const QString CodeInterpreterFunction = R"(javascript_interpret)";
const QString CodeInterpreterTag = R"(<)" + CodeInterpreterFunction + R"(>)";
const QString CodeInterpreterEndTag = R"(</)" + CodeInterpreterFunction + R"(>)";
const QString CodeInterpreterPrefix = CodeInterpreterTag + "\n```javascript\n";
const QString CodeInterpreterSuffix = "```\n" + CodeInterpreterEndTag;
}
class ToolCallParser
{
public:
ToolCallParser();
ToolCallParser(const QStringList &tagNames);
void reset();
void update(const QString &update);
QString buffer() const { return m_buffer; }
QString toolCall() const { return m_toolCall; }
void update(const QByteArray &update);
QString toolCall() const { return QString::fromUtf8(m_toolCall); }
int startIndex() const { return m_startIndex; }
ToolEnums::ParseState state() const { return m_state; }
QByteArray startTag() const;
QByteArray endTag() const;
// Splits
QPair<QString, QString> split();
bool hasSplit() const { return m_hasSplit; }
bool splitIfPossible();
QStringList buffers() const;
int numberOfBuffers() const { return m_buffers.size(); }
static QString makeStartTag(const QString &name) { return u"<%1>"_s .arg(name); }
static QString makeEndTag (const QString &name) { return u"</%1>"_s.arg(name); }
private:
QByteArray &currentBuffer();
void resetSearchState();
bool isExpected(char c) const;
void setExpected(const QList<QByteArray> &tags);
QChar m_expected;
QList<QByteArray> m_possibleStartTags;
QList<QByteArray> m_possibleEndTags;
QByteArray m_startTagBuffer;
QByteArray m_endTagBuffer;
int m_currentTagIndex;
QList<char> m_expected;
int m_expectedIndex;
ToolEnums::ParseState m_state;
QString m_buffer;
QString m_toolCall;
QString m_endTagBuffer;
QList<QByteArray> m_buffers;
QByteArray m_toolCall;
int m_startIndex;
bool m_hasSplit;
int m_endIndex;
};
namespace ToolCallConstants
{
// NB: the parsing code assumes the first char of the various tags differ
inline const QString CodeInterpreterFunction = u"javascript_interpret"_s;
inline const QString CodeInterpreterStartTag = ToolCallParser::makeStartTag(CodeInterpreterFunction);
inline const QString CodeInterpreterEndTag = ToolCallParser::makeEndTag (CodeInterpreterFunction);
inline const QString CodeInterpreterPrefix = u"%1\n```javascript\n"_s.arg(CodeInterpreterStartTag);
inline const QString CodeInterpreterSuffix = u"```\n%1"_s .arg(CodeInterpreterEndTag );
inline const QString ThinkTagName = u"think"_s;
inline const QString ThinkStartTag = ToolCallParser::makeStartTag(ThinkTagName);
inline const QString ThinkEndTag = ToolCallParser::makeEndTag (ThinkTagName);
inline const QStringList AllTagNames { CodeInterpreterFunction, ThinkTagName };
}
#endif // TOOLCALLPARSER_H