mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2026-07-17 10:58:08 +00:00
Compare commits
6 Commits
cmake/besp
...
polymorphi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0efb9d4c0 | ||
|
|
62cab695eb | ||
|
|
861453c4d7 | ||
|
|
b19db6c20d | ||
|
|
da00527101 | ||
|
|
57c0974f4a |
@@ -2,4 +2,4 @@
|
||||
[flake8]
|
||||
exclude = .*,__pycache__
|
||||
max-line-length = 120
|
||||
extend-ignore = B001,C408,D,DAR,E221,E303,E722,E741,E800,N801,N806,P101,S101,S324,S404,S406,S410,S603,WPS100,WPS110,WPS111,WPS113,WPS114,WPS115,WPS120,WPS2,WPS300,WPS301,WPS304,WPS305,WPS306,WPS309,WPS316,WPS317,WPS318,WPS319,WPS322,WPS323,WPS326,WPS329,WPS330,WPS332,WPS336,WPS337,WPS347,WPS360,WPS361,WPS414,WPS420,WPS421,WPS429,WPS430,WPS431,WPS432,WPS433,WPS437,WPS440,WPS440,WPS441,WPS442,WPS457,WPS458,WPS460,WPS462,WPS463,WPS473,WPS501,WPS504,WPS505,WPS508,WPS509,WPS510,WPS515,WPS516,WPS519,WPS529,WPS531,WPS602,WPS604,WPS605,WPS608,WPS609,WPS613,WPS615
|
||||
extend-ignore = B001,C408,D,DAR,E221,E303,E722,E741,E800,N801,N806,P101,S101,S324,S404,S406,S410,S603,WPS100,WPS110,WPS111,WPS113,WPS114,WPS115,WPS120,WPS2,WPS300,WPS301,WPS304,WPS305,WPS306,WPS309,WPS316,WPS317,WPS318,WPS319,WPS322,WPS323,WPS326,WPS329,WPS330,WPS332,WPS336,WPS337,WPS347,WPS360,WPS361,WPS407,WPS414,WPS420,WPS421,WPS429,WPS430,WPS431,WPS432,WPS433,WPS437,WPS440,WPS440,WPS441,WPS442,WPS457,WPS458,WPS460,WPS462,WPS463,WPS473,WPS501,WPS504,WPS505,WPS508,WPS509,WPS510,WPS515,WPS516,WPS519,WPS520,WPS529,WPS531,WPS602,WPS604,WPS605,WPS608,WPS609,WPS613,WPS615
|
||||
|
||||
@@ -6,9 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Add ability to attach text, markdown, and rst files to chat ([#3135](https://github.com/nomic-ai/gpt4all/pull/3135))
|
||||
- Add feature to minimize to system tray (by [@bgallois](https://github.com/bgallois) in ([#3109](https://github.com/nomic-ai/gpt4all/pull/3109))
|
||||
|
||||
### Changed
|
||||
- Implement Qt 6.8 compatibility ([#3121](https://github.com/nomic-ai/gpt4all/pull/3121))
|
||||
|
||||
### Fixed
|
||||
- Fix bug in GUI when localdocs encounters binary data ([#3137](https://github.com/nomic-ai/gpt4all/pull/3137))
|
||||
- Fix LocalDocs bugs that prevented some docx files from fully chunking ([#3140](https://github.com/nomic-ai/gpt4all/pull/3140))
|
||||
|
||||
## [3.4.2] - 2024-10-16
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -75,6 +75,7 @@ configure_file(
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/config.h"
|
||||
)
|
||||
|
||||
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
|
||||
find_package(Qt6 6.5 COMPONENTS Core HttpServer LinguistTools Pdf Quick QuickDialogs2 Sql Svg REQUIRED)
|
||||
|
||||
if (QT_KNOWN_POLICY_QTP0004)
|
||||
@@ -102,10 +103,30 @@ add_subdirectory(../gpt4all-backend llmodel)
|
||||
|
||||
if (GPT4ALL_TEST)
|
||||
enable_testing()
|
||||
|
||||
# Llama-3.2-1B model
|
||||
set(TEST_MODEL "Llama-3.2-1B-Instruct-Q4_0.gguf")
|
||||
set(TEST_MODEL_MD5 "48ff0243978606fdba19d899b77802fc")
|
||||
set(TEST_MODEL_PATH "${CMAKE_BINARY_DIR}/resources/${TEST_MODEL}")
|
||||
set(TEST_MODEL_URL "https://huggingface.co/bartowski/Llama-3.2-1B-Instruct-GGUF/resolve/main/${TEST_MODEL}")
|
||||
|
||||
# Create a custom command to download the file if it does not exist or if the checksum does not match
|
||||
add_custom_command(
|
||||
OUTPUT "${TEST_MODEL_PATH}"
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Downloading test model from ${TEST_MODEL_URL} ..."
|
||||
COMMAND ${CMAKE_COMMAND} -DURL="${TEST_MODEL_URL}" -DOUTPUT_PATH="${TEST_MODEL_PATH}" -DEXPECTED_MD5="${TEST_MODEL_MD5}" -P "${CMAKE_SOURCE_DIR}/cmake/download_model.cmake"
|
||||
DEPENDS "${CMAKE_SOURCE_DIR}/cmake/download_model.cmake"
|
||||
)
|
||||
|
||||
# Define a custom target that depends on the downloaded model
|
||||
add_custom_target(download_test_model
|
||||
DEPENDS "${TEST_MODEL_PATH}"
|
||||
)
|
||||
|
||||
add_subdirectory(tests)
|
||||
|
||||
# The 'check' target makes sure the tests and their dependencies are up-to-date before running them
|
||||
add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure DEPENDS chat gpt4all_tests)
|
||||
add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure DEPENDS download_test_model chat gpt4all_tests)
|
||||
endif()
|
||||
|
||||
set(CHAT_EXE_RESOURCES)
|
||||
@@ -152,6 +173,12 @@ if (APPLE)
|
||||
set_source_files_properties(${CHAT_EXE_RESOURCES} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
endif()
|
||||
|
||||
set(MACOS_SOURCES)
|
||||
if (APPLE)
|
||||
find_library(COCOA_LIBRARY Cocoa)
|
||||
list(APPEND MACOS_SOURCES src/macosdock.mm src/macosdock.h)
|
||||
endif()
|
||||
|
||||
qt_add_executable(chat
|
||||
src/main.cpp
|
||||
src/chat.cpp src/chat.h
|
||||
@@ -173,6 +200,7 @@ qt_add_executable(chat
|
||||
src/server.cpp src/server.h
|
||||
src/xlsxtomd.cpp src/xlsxtomd.h
|
||||
${CHAT_EXE_RESOURCES}
|
||||
${MACOS_SOURCES}
|
||||
)
|
||||
gpt4all_add_warning_options(chat)
|
||||
|
||||
@@ -210,6 +238,7 @@ qt_add_qml_module(chat
|
||||
qml/MyDialog.qml
|
||||
qml/MyDirectoryField.qml
|
||||
qml/MyFileDialog.qml
|
||||
qml/MyFileIcon.qml
|
||||
qml/MyFolderDialog.qml
|
||||
qml/MyFancyLink.qml
|
||||
qml/MyMenu.qml
|
||||
@@ -244,6 +273,7 @@ qt_add_qml_module(chat
|
||||
icons/eject.svg
|
||||
icons/email.svg
|
||||
icons/file-doc.svg
|
||||
icons/file-docx.svg
|
||||
icons/file-md.svg
|
||||
icons/file-pdf.svg
|
||||
icons/file-txt.svg
|
||||
@@ -357,6 +387,9 @@ target_link_libraries(chat
|
||||
target_link_libraries(chat
|
||||
PRIVATE llmodel SingleApplication fmt::fmt duckx::duckx QXlsx)
|
||||
|
||||
if (APPLE)
|
||||
target_link_libraries(chat PRIVATE ${COCOA_LIBRARY})
|
||||
endif()
|
||||
|
||||
# -- install --
|
||||
|
||||
|
||||
12
gpt4all-chat/cmake/download_model.cmake
Normal file
12
gpt4all-chat/cmake/download_model.cmake
Normal file
@@ -0,0 +1,12 @@
|
||||
if(NOT DEFINED URL OR NOT DEFINED OUTPUT_PATH OR NOT DEFINED EXPECTED_MD5)
|
||||
message(FATAL_ERROR "Usage: cmake -DURL=<url> -DOUTPUT_PATH=<path> -DEXPECTED_MD5=<md5> -P download_model.cmake")
|
||||
endif()
|
||||
|
||||
message(STATUS "Downloading model from ${URL} to ${OUTPUT_PATH} ...")
|
||||
|
||||
file(DOWNLOAD "${URL}" "${OUTPUT_PATH}" EXPECTED_MD5 "${EXPECTED_MD5}" STATUS status)
|
||||
|
||||
list(GET status 0 status_code)
|
||||
if(NOT status_code EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to download model: ${status}")
|
||||
endif()
|
||||
@@ -3,7 +3,7 @@ set(BUILD_SHARED_LIBS OFF)
|
||||
set(FMT_INSTALL OFF)
|
||||
add_subdirectory(fmt)
|
||||
|
||||
set(QAPPLICATION_CLASS QGuiApplication)
|
||||
set(QAPPLICATION_CLASS QApplication)
|
||||
add_subdirectory(SingleApplication)
|
||||
|
||||
set(DUCKX_INSTALL OFF)
|
||||
|
||||
1
gpt4all-chat/icons/file-docx.svg
Normal file
1
gpt4all-chat/icons/file-docx.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="152" y1="96" x2="208" y2="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="152" y1="160" x2="208" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path d="M64,72V40a8,8,0,0,1,8-8H200a8,8,0,0,1,8,8V216a8,8,0,0,1-8,8H72a8,8,0,0,1-8-8V184" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><polyline points="64 104 76 152 92 120 108 152 120 104" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><rect x="32" y="72" width="120" height="112" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
|
After Width: | Height: | Size: 893 B |
@@ -12,6 +12,7 @@ import network
|
||||
import gpt4all
|
||||
import localdocs
|
||||
import mysettings
|
||||
import Qt.labs.platform
|
||||
|
||||
Window {
|
||||
id: window
|
||||
@@ -22,6 +23,43 @@ Window {
|
||||
visible: true
|
||||
title: qsTr("GPT4All v%1").arg(Qt.application.version)
|
||||
|
||||
SystemTrayIcon {
|
||||
id: systemTrayIcon
|
||||
property bool shouldClose: false
|
||||
visible: MySettings.systemTray && !shouldClose
|
||||
icon.source: "qrc:/gpt4all/icons/gpt4all.svg"
|
||||
|
||||
function restore() {
|
||||
LLM.showDockIcon();
|
||||
window.show();
|
||||
window.raise();
|
||||
window.requestActivate();
|
||||
}
|
||||
onActivated: function(reason) {
|
||||
if (reason === SystemTrayIcon.Context && Qt.platform.os !== "osx")
|
||||
menu.open();
|
||||
else if (reason === SystemTrayIcon.Trigger)
|
||||
restore();
|
||||
}
|
||||
|
||||
menu: Menu {
|
||||
MenuItem {
|
||||
text: qsTr("Restore")
|
||||
onTriggered: systemTrayIcon.restore()
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Quit")
|
||||
onTriggered: {
|
||||
systemTrayIcon.restore();
|
||||
systemTrayIcon.shouldClose = true;
|
||||
window.shouldClose = true;
|
||||
savingPopup.open();
|
||||
ChatListModel.saveChats();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Settings {
|
||||
property alias x: window.x
|
||||
property alias y: window.y
|
||||
@@ -156,7 +194,7 @@ Window {
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
}
|
||||
|
||||
property bool hasSaved: false
|
||||
property bool shouldClose: false
|
||||
|
||||
PopupDialog {
|
||||
id: savingPopup
|
||||
@@ -180,9 +218,18 @@ Window {
|
||||
}
|
||||
|
||||
onClosing: function(close) {
|
||||
if (window.hasSaved)
|
||||
if (systemTrayIcon.visible) {
|
||||
LLM.hideDockIcon();
|
||||
window.visible = false;
|
||||
ChatListModel.saveChats();
|
||||
close.accepted = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.shouldClose)
|
||||
return;
|
||||
|
||||
window.shouldClose = true;
|
||||
savingPopup.open();
|
||||
ChatListModel.saveChats();
|
||||
close.accepted = false
|
||||
@@ -191,9 +238,9 @@ Window {
|
||||
Connections {
|
||||
target: ChatListModel
|
||||
function onSaveChatsFinished() {
|
||||
window.hasSaved = true;
|
||||
savingPopup.close();
|
||||
window.close()
|
||||
if (window.shouldClose)
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -504,17 +504,34 @@ MySettingsTab {
|
||||
}
|
||||
}
|
||||
MySettingsLabel {
|
||||
id: serverChatLabel
|
||||
text: qsTr("Enable Local API Server")
|
||||
helpText: qsTr("Expose an OpenAI-Compatible server to localhost. WARNING: Results in increased resource usage.")
|
||||
id: trayLabel
|
||||
text: qsTr("Enable System Tray")
|
||||
helpText: qsTr("The application will minimize to the system tray when the window is closed.")
|
||||
Layout.row: 13
|
||||
Layout.column: 0
|
||||
}
|
||||
MyCheckBox {
|
||||
id: serverChatBox
|
||||
id: trayBox
|
||||
Layout.row: 13
|
||||
Layout.column: 2
|
||||
Layout.alignment: Qt.AlignRight
|
||||
checked: MySettings.systemTray
|
||||
onClicked: {
|
||||
MySettings.systemTray = !MySettings.systemTray
|
||||
}
|
||||
}
|
||||
MySettingsLabel {
|
||||
id: serverChatLabel
|
||||
text: qsTr("Enable Local API Server")
|
||||
helpText: qsTr("Expose an OpenAI-Compatible server to localhost. WARNING: Results in increased resource usage.")
|
||||
Layout.row: 14
|
||||
Layout.column: 0
|
||||
}
|
||||
MyCheckBox {
|
||||
id: serverChatBox
|
||||
Layout.row: 14
|
||||
Layout.column: 2
|
||||
Layout.alignment: Qt.AlignRight
|
||||
checked: MySettings.serverChat
|
||||
onClicked: {
|
||||
MySettings.serverChat = !MySettings.serverChat
|
||||
@@ -524,7 +541,7 @@ MySettingsTab {
|
||||
id: serverPortLabel
|
||||
text: qsTr("API Server Port")
|
||||
helpText: qsTr("The port to use for the local server. Requires restart.")
|
||||
Layout.row: 14
|
||||
Layout.row: 15
|
||||
Layout.column: 0
|
||||
}
|
||||
MyTextField {
|
||||
@@ -532,7 +549,7 @@ MySettingsTab {
|
||||
text: MySettings.networkPort
|
||||
color: theme.textColor
|
||||
font.pixelSize: theme.fontSizeLarge
|
||||
Layout.row: 14
|
||||
Layout.row: 15
|
||||
Layout.column: 2
|
||||
Layout.minimumWidth: 200
|
||||
Layout.maximumWidth: 200
|
||||
@@ -577,12 +594,12 @@ MySettingsTab {
|
||||
id: updatesLabel
|
||||
text: qsTr("Check For Updates")
|
||||
helpText: qsTr("Manually check for an update to GPT4All.");
|
||||
Layout.row: 15
|
||||
Layout.row: 16
|
||||
Layout.column: 0
|
||||
}
|
||||
|
||||
MySettingsButton {
|
||||
Layout.row: 15
|
||||
Layout.row: 16
|
||||
Layout.column: 2
|
||||
Layout.alignment: Qt.AlignRight
|
||||
text: qsTr("Updates");
|
||||
@@ -593,7 +610,7 @@ MySettingsTab {
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.row: 16
|
||||
Layout.row: 17
|
||||
Layout.column: 0
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillWidth: true
|
||||
|
||||
@@ -915,30 +915,12 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
|
||||
Item {
|
||||
id: attachmentFileIcon
|
||||
width: 40
|
||||
height: 40
|
||||
Image {
|
||||
id: fileIcon
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
sourceSize.width: 40
|
||||
sourceSize.height: 40
|
||||
mipmap: true
|
||||
source: {
|
||||
return "qrc:/gpt4all/icons/file-xls.svg"
|
||||
}
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: fileIcon
|
||||
source: fileIcon
|
||||
color: theme.textColor
|
||||
}
|
||||
MyFileIcon {
|
||||
iconSize: 40
|
||||
fileName: modelData.file
|
||||
}
|
||||
|
||||
Text {
|
||||
id: attachmentFileText
|
||||
width: 295
|
||||
height: 40
|
||||
text: modelData.file
|
||||
@@ -1343,32 +1325,11 @@ Rectangle {
|
||||
id: title
|
||||
spacing: 5
|
||||
Layout.maximumWidth: 180
|
||||
Item {
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
Image {
|
||||
id: fileIcon
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
sourceSize.width: 24
|
||||
sourceSize.height: 24
|
||||
mipmap: true
|
||||
source: {
|
||||
if (modelData.file.toLowerCase().endsWith(".txt"))
|
||||
return "qrc:/gpt4all/icons/file-txt.svg"
|
||||
else if (modelData.file.toLowerCase().endsWith(".pdf"))
|
||||
return "qrc:/gpt4all/icons/file-pdf.svg"
|
||||
else if (modelData.file.toLowerCase().endsWith(".md"))
|
||||
return "qrc:/gpt4all/icons/file-md.svg"
|
||||
else
|
||||
return "qrc:/gpt4all/icons/file.svg"
|
||||
}
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: fileIcon
|
||||
source: fileIcon
|
||||
color: theme.textColor
|
||||
}
|
||||
MyFileIcon {
|
||||
iconSize: 24
|
||||
fileName: modelData.file
|
||||
Layout.preferredWidth: iconSize
|
||||
Layout.preferredHeight: iconSize
|
||||
}
|
||||
Text {
|
||||
Layout.maximumWidth: 156
|
||||
@@ -1949,30 +1910,12 @@ Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
|
||||
Item {
|
||||
id: attachmentFileIcon2
|
||||
width: 40
|
||||
height: 40
|
||||
Image {
|
||||
id: fileIcon2
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
sourceSize.width: 40
|
||||
sourceSize.height: 40
|
||||
mipmap: true
|
||||
source: {
|
||||
return "qrc:/gpt4all/icons/file-xls.svg"
|
||||
}
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: fileIcon2
|
||||
source: fileIcon2
|
||||
color: theme.textColor
|
||||
}
|
||||
MyFileIcon {
|
||||
iconSize: 40
|
||||
fileName: model.file
|
||||
}
|
||||
|
||||
Text {
|
||||
id: attachmentFileText2
|
||||
width: 265
|
||||
height: 40
|
||||
text: model.file
|
||||
@@ -2188,7 +2131,7 @@ Rectangle {
|
||||
|
||||
MyFileDialog {
|
||||
id: fileDialog
|
||||
nameFilters: ["Excel files (*.xlsx)"]
|
||||
nameFilters: ["All Supported Files (*.txt *.md *.rst *.xlsx)", "Text Files (*.txt *.md *.rst)", "Excel Worksheets (*.xlsx)"]
|
||||
}
|
||||
|
||||
MyMenu {
|
||||
|
||||
41
gpt4all-chat/qml/MyFileIcon.qml
Normal file
41
gpt4all-chat/qml/MyFileIcon.qml
Normal file
@@ -0,0 +1,41 @@
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Basic
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
Item {
|
||||
id: fileIcon
|
||||
property real iconSize: 24
|
||||
property string fileName: ""
|
||||
implicitWidth: iconSize
|
||||
implicitHeight: iconSize
|
||||
|
||||
Image {
|
||||
id: fileImage
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
sourceSize.width: iconSize
|
||||
sourceSize.height: iconSize
|
||||
mipmap: true
|
||||
source: {
|
||||
if (fileIcon.fileName.toLowerCase().endsWith(".txt"))
|
||||
return "qrc:/gpt4all/icons/file-txt.svg"
|
||||
else if (fileIcon.fileName.toLowerCase().endsWith(".pdf"))
|
||||
return "qrc:/gpt4all/icons/file-pdf.svg"
|
||||
else if (fileIcon.fileName.toLowerCase().endsWith(".md"))
|
||||
return "qrc:/gpt4all/icons/file-md.svg"
|
||||
else if (fileIcon.fileName.toLowerCase().endsWith(".xlsx"))
|
||||
return "qrc:/gpt4all/icons/file-xls.svg"
|
||||
else if (fileIcon.fileName.toLowerCase().endsWith(".docx"))
|
||||
return "qrc:/gpt4all/icons/file-docx.svg"
|
||||
else
|
||||
return "qrc:/gpt4all/icons/file.svg"
|
||||
}
|
||||
}
|
||||
ColorOverlay {
|
||||
anchors.fill: fileImage
|
||||
source: fileImage
|
||||
color: theme.textColor
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,13 @@ void Chat::newPromptResponsePair(const QString &prompt, const QList<QUrl> &attac
|
||||
Q_ASSERT(url.isLocalFile());
|
||||
const QString localFilePath = url.toLocalFile();
|
||||
const QFileInfo info(localFilePath);
|
||||
Q_ASSERT(info.suffix() == "xlsx"); // We only support excel right now
|
||||
|
||||
Q_ASSERT(
|
||||
info.suffix().toLower() == "xlsx" ||
|
||||
info.suffix().toLower() == "txt" ||
|
||||
info.suffix().toLower() == "md" ||
|
||||
info.suffix().toLower() == "rst"
|
||||
);
|
||||
|
||||
PromptAttachment attached;
|
||||
attached.url = url;
|
||||
|
||||
@@ -130,6 +130,7 @@ public:
|
||||
QList<QString> generatedQuestions() const { return m_generatedQuestions; }
|
||||
|
||||
bool needsSave() const { return m_needsSave; }
|
||||
void setNeedsSave(bool n) { m_needsSave = n; }
|
||||
|
||||
public Q_SLOTS:
|
||||
void serverNewPromptResponsePair(const QString &prompt, const QList<PromptAttachment> &attachments = {});
|
||||
|
||||
@@ -51,6 +51,10 @@ void ChatListModel::loadChats()
|
||||
connect(thread, &ChatsRestoreThread::finished, thread, &QObject::deleteLater);
|
||||
thread->start();
|
||||
|
||||
ChatSaver *saver = new ChatSaver;
|
||||
connect(this, &ChatListModel::requestSaveChats, saver, &ChatSaver::saveChats, Qt::QueuedConnection);
|
||||
connect(saver, &ChatSaver::saveChatsFinished, this, &ChatListModel::saveChatsFinished, Qt::QueuedConnection);
|
||||
|
||||
connect(MySettings::globalInstance(), &MySettings::serverChatChanged, this, &ChatListModel::handleServerEnabledChanged);
|
||||
}
|
||||
|
||||
@@ -88,9 +92,6 @@ void ChatListModel::saveChats()
|
||||
return;
|
||||
}
|
||||
|
||||
ChatSaver *saver = new ChatSaver;
|
||||
connect(this, &ChatListModel::requestSaveChats, saver, &ChatSaver::saveChats, Qt::QueuedConnection);
|
||||
connect(saver, &ChatSaver::saveChatsFinished, this, &ChatListModel::saveChatsFinished, Qt::QueuedConnection);
|
||||
emit requestSaveChats(toSave);
|
||||
}
|
||||
|
||||
@@ -128,6 +129,7 @@ void ChatSaver::saveChats(const QVector<Chat *> &chats)
|
||||
continue;
|
||||
}
|
||||
|
||||
chat->setNeedsSave(false);
|
||||
if (originalFile.exists())
|
||||
originalFile.remove();
|
||||
tempFile.rename(filePath);
|
||||
|
||||
@@ -33,7 +33,6 @@ class ChatSaver : public QObject
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ChatSaver();
|
||||
void stop();
|
||||
|
||||
Q_SIGNALS:
|
||||
void saveChatsFinished();
|
||||
@@ -238,7 +237,6 @@ public Q_SLOTS:
|
||||
Q_SIGNALS:
|
||||
void countChanged();
|
||||
void currentChatChanged();
|
||||
void chatsSavedFinished();
|
||||
void requestSaveChats(const QVector<Chat*> &);
|
||||
void saveChatsFinished();
|
||||
|
||||
|
||||
@@ -40,6 +40,11 @@ public:
|
||||
|
||||
QString processedContent() const
|
||||
{
|
||||
const QString localFilePath = url.toLocalFile();
|
||||
const QFileInfo info(localFilePath);
|
||||
if (info.suffix().toLower() != "xlsx")
|
||||
return u"## Attached: %1\n\n%2"_s.arg(file(), content);
|
||||
|
||||
QBuffer buffer;
|
||||
buffer.setData(content);
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
|
||||
@@ -1208,11 +1208,14 @@ protected:
|
||||
qsizetype wordEnd = wordStart + 1;
|
||||
while (wordEnd >= m_buffer.size() || !m_buffer[wordEnd].isSpace()) {
|
||||
if (wordEnd >= m_buffer.size() && !fillBuffer())
|
||||
return std::nullopt;
|
||||
break;
|
||||
if (!m_buffer[wordEnd].isSpace())
|
||||
++wordEnd;
|
||||
}
|
||||
|
||||
if (wordStart == wordEnd)
|
||||
return std::nullopt;
|
||||
|
||||
auto size = wordEnd - wordStart;
|
||||
QString word = std::move(m_buffer);
|
||||
m_buffer = word.sliced(wordStart + size);
|
||||
@@ -1220,7 +1223,6 @@ protected:
|
||||
word.resize(size);
|
||||
else
|
||||
word = word.sliced(wordStart, size);
|
||||
|
||||
return word;
|
||||
}
|
||||
|
||||
@@ -1232,18 +1234,30 @@ protected:
|
||||
// try next paragraph
|
||||
if (!m_paragraph->has_next())
|
||||
return false;
|
||||
|
||||
m_paragraph->next();
|
||||
m_buffer += u'\n';
|
||||
}
|
||||
|
||||
bool foundText = false;
|
||||
auto &run = m_run->get_node();
|
||||
const char *text = run.child("w:t").text().get();
|
||||
if (!*text && run.child("w:tab"))
|
||||
text = "\t";
|
||||
m_run->next();
|
||||
if (*text) {
|
||||
m_buffer += QUtf8StringView(text);
|
||||
return true;
|
||||
for (auto node = run.first_child(); node; node = node.next_sibling()) {
|
||||
std::string node_name = node.name();
|
||||
if (node_name == "w:t") {
|
||||
const char *text = node.text().get();
|
||||
if (*text) {
|
||||
foundText = true;
|
||||
m_buffer += QUtf8StringView(text);
|
||||
}
|
||||
} else if (node_name == "w:br") {
|
||||
m_buffer += u'\n';
|
||||
} else if (node_name == "w:tab") {
|
||||
m_buffer += u'\t';
|
||||
}
|
||||
}
|
||||
|
||||
m_run->next();
|
||||
if (foundText) return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1355,7 +1369,8 @@ ChunkStreamer::Status ChunkStreamer::step()
|
||||
for (;;) {
|
||||
if (auto error = m_reader->getError()) {
|
||||
m_docKey.reset(); // done processing
|
||||
return *error;
|
||||
retval = *error;
|
||||
break;
|
||||
}
|
||||
|
||||
// get a word, if needed
|
||||
@@ -1515,8 +1530,22 @@ void Database::handleEmbeddingsGenerated(const QVector<EmbeddingResult> &embeddi
|
||||
for (const auto &[key, stat]: std::as_const(stats).asKeyValueRange()) {
|
||||
if (!m_collectionMap.contains(key.folder_id)) continue;
|
||||
CollectionItem item = guiCollectionItem(key.folder_id);
|
||||
item.currentEmbeddingsToIndex -= stat.nAdded + stat.nSkipped;
|
||||
item.totalEmbeddingsToIndex -= stat.nSkipped;
|
||||
Q_ASSERT(item.currentEmbeddingsToIndex >= stat.nAdded + stat.nSkipped);
|
||||
if (item.currentEmbeddingsToIndex < stat.nAdded + stat.nSkipped) {
|
||||
qWarning() << "Database ERROR: underflow in current embeddings to index statistics";
|
||||
item.currentEmbeddingsToIndex = 0;
|
||||
} else {
|
||||
item.currentEmbeddingsToIndex -= stat.nAdded + stat.nSkipped;
|
||||
}
|
||||
|
||||
Q_ASSERT(item.totalEmbeddingsToIndex >= stat.nSkipped);
|
||||
if (item.totalEmbeddingsToIndex < stat.nSkipped) {
|
||||
qWarning() << "Database ERROR: underflow in total embeddings to index statistics";
|
||||
item.totalEmbeddingsToIndex = 0;
|
||||
} else {
|
||||
item.totalEmbeddingsToIndex -= stat.nSkipped;
|
||||
}
|
||||
|
||||
if (!stat.lastFile.isNull())
|
||||
item.fileCurrentlyProcessing = stat.lastFile;
|
||||
|
||||
@@ -1746,7 +1775,13 @@ void Database::scanQueue()
|
||||
|
||||
dequeue:
|
||||
auto item = guiCollectionItem(folder_id);
|
||||
item.currentBytesToIndex -= info.file.size();
|
||||
Q_ASSERT(item.currentBytesToIndex >= info.file.size());
|
||||
if (item.currentBytesToIndex < info.file.size()) {
|
||||
qWarning() << "Database ERROR: underflow in current bytes to index statistics";
|
||||
item.currentBytesToIndex = 0;
|
||||
} else {
|
||||
item.currentBytesToIndex -= info.file.size();
|
||||
}
|
||||
updateGuiForCollectionItem(item);
|
||||
return updateFolderToIndex(folder_id, countForFolder);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
# include "network.h"
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_MAC
|
||||
#include "macosdock.h"
|
||||
#endif
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
class MyLLM: public LLM { };
|
||||
@@ -105,3 +109,21 @@ bool LLM::isNetworkOnline() const
|
||||
auto * netinfo = QNetworkInformation::instance();
|
||||
return !netinfo || netinfo->reachability() == QNetworkInformation::Reachability::Online;
|
||||
}
|
||||
|
||||
void LLM::showDockIcon() const
|
||||
{
|
||||
#ifdef Q_OS_MAC
|
||||
MacOSDock::showIcon();
|
||||
#else
|
||||
qt_noop();
|
||||
#endif
|
||||
}
|
||||
|
||||
void LLM::hideDockIcon() const
|
||||
{
|
||||
#ifdef Q_OS_MAC
|
||||
MacOSDock::hideIcon();
|
||||
#else
|
||||
qt_noop();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ public:
|
||||
Q_INVOKABLE QString systemTotalRAMInGBString() const;
|
||||
Q_INVOKABLE bool isNetworkOnline() const;
|
||||
|
||||
Q_INVOKABLE void showDockIcon() const;
|
||||
Q_INVOKABLE void hideDockIcon() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void isNetworkOnlineChanged();
|
||||
|
||||
|
||||
9
gpt4all-chat/src/macosdock.h
Normal file
9
gpt4all-chat/src/macosdock.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifndef MACOSDOCK_H
|
||||
#define MACOSDOCK_H
|
||||
|
||||
struct MacOSDock {
|
||||
static void showIcon();
|
||||
static void hideIcon();
|
||||
};
|
||||
|
||||
#endif // MACOSDOCK_H
|
||||
13
gpt4all-chat/src/macosdock.mm
Normal file
13
gpt4all-chat/src/macosdock.mm
Normal file
@@ -0,0 +1,13 @@
|
||||
#include "macosdock.h"
|
||||
|
||||
#include <Cocoa/Cocoa.h>
|
||||
|
||||
void MacOSDock::showIcon()
|
||||
{
|
||||
[[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
}
|
||||
|
||||
void MacOSDock::hideIcon()
|
||||
{
|
||||
[[NSApplication sharedApplication] setActivationPolicy:NSApplicationActivationPolicyProhibited];
|
||||
}
|
||||
@@ -44,6 +44,7 @@ static void raiseWindow(QWindow *window)
|
||||
|
||||
SetForegroundWindow(hwnd);
|
||||
#else
|
||||
LLM::globalInstance()->showDockIcon();
|
||||
window->show();
|
||||
window->raise();
|
||||
window->requestActivate();
|
||||
|
||||
141
gpt4all-chat/src/message_content.cpp
Normal file
141
gpt4all-chat/src/message_content.cpp
Normal file
@@ -0,0 +1,141 @@
|
||||
#include <QString>
|
||||
#include <QDataStream>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#define THROW_IF_BAD(stream) \
|
||||
do { \
|
||||
if (auto status = (stream).status(); status != QDataStream::Status::OK) \
|
||||
throw std::runtime_error(fmt::format("bad stream status: {}", status)); \
|
||||
} while (0) \
|
||||
|
||||
inline namespace MessageEnums {
|
||||
Q_NAMESPACE
|
||||
// for DataLake
|
||||
enum class MessageRating : quint8 { Unrated = 0, Positive = 1, Negative = 2, Max = Negative };
|
||||
Q_ENUM_NS(MessageRating)
|
||||
}
|
||||
|
||||
// TODO(Adam): Maybe we should include the model name here as well as timestamp?
|
||||
class MessageContent {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString role READ role CONSTANT)
|
||||
Q_PROPERTY(QString content MEMBER content)
|
||||
|
||||
protected:
|
||||
enum class Type : quint8 { Prompt = 0, Response = 1, Max = Response };
|
||||
|
||||
public:
|
||||
virtual QString role() const = 0;
|
||||
|
||||
friend auto deserialize(QDataStream &stream, ChatModel *model) -> std::unique_ptr<MessageContent>
|
||||
{
|
||||
union { quint8 u8; };
|
||||
THROW_IF_BAD(stream);
|
||||
|
||||
stream >> u8; // version
|
||||
THROW_IF_BAD(stream);
|
||||
if (u8 > VERSION)
|
||||
throw std::invalid_argument(fmt::format("unknown version: {}", u8));
|
||||
|
||||
stream >> u8; // type
|
||||
THROW_IF_BAD(stream);
|
||||
if (u8 > Type::Max)
|
||||
throw std::invalid_argument(fmt::format("unknown type: {}", u8));
|
||||
auto type = Type(u8);
|
||||
|
||||
std::unique_ptr<MessageContent> result;
|
||||
switch (type) {
|
||||
case Prompt: result = std::make_unique<PromptContent> (); break;
|
||||
case Response: result = std::make_unique<ResponseContent>(model); break;
|
||||
}
|
||||
|
||||
stream >> result->content;
|
||||
THROW_IF_BAD(stream);
|
||||
// TODO: add more common fields as needed
|
||||
|
||||
result->deserializeInternal(stream, version);
|
||||
return result;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void deserializeInternal(QDataStream &stream, quint32 version) = 0;
|
||||
|
||||
public:
|
||||
QString content;
|
||||
|
||||
private:
|
||||
static quint8 VERSION = 0;
|
||||
};
|
||||
Q_DECLARE_METATYPE(MessageContent)
|
||||
|
||||
class PromptContent final : public MessageContent {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QList<ResultInfo> sources MEMBER sources)
|
||||
Q_PROPERTY(QList<ResultInfo> consolidatedSources MEMBER consolidatedSources)
|
||||
Q_PROPERTY(QList<PromptAttachment> promptAttachments MEMBER promptAttachments)
|
||||
Q_PROPERTY(QString promptPlusAttachments READ promptPlusAttachments)
|
||||
|
||||
public:
|
||||
QString role() const override { return u"user"_s; }
|
||||
|
||||
QString promptPlusAttachments() const
|
||||
{
|
||||
if (!promptAttachments.isEmpty()) {
|
||||
QStringList items;
|
||||
for (auto &attached : std::as_const(promptAttachments))
|
||||
items << attached.processedContent();
|
||||
items << content;
|
||||
return items.join("\n\n");
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
protected:
|
||||
void deserializeInternal(QDataStream &stream, quint32 version) override
|
||||
{
|
||||
Q_UNUSED(version); // only v0 exists currently
|
||||
// TODO: ...
|
||||
}
|
||||
};
|
||||
Q_DECLARE_METATYPE(PromptContent)
|
||||
|
||||
class ResponseContent final : public MessageContent {
|
||||
Q_GADGET
|
||||
Q_PROPERTY(QString newResponse MEMBER newResponse) // for DataLake
|
||||
Q_PROPERTY(bool currentResponse READ currentResponse)
|
||||
Q_PROPERTY(bool stopped MEMBER stopped) // for DataLake
|
||||
Q_PROPERTY(MessageRating rating MEMBER rating) // for DataLake
|
||||
|
||||
public:
|
||||
explicit ResponseContent(ChatModel *model)
|
||||
: m_model(model) {}
|
||||
|
||||
QString role() const override { return u"assistant"_s; }
|
||||
bool currentResponse() const { return this == m_model->currentResponse(); }
|
||||
|
||||
protected:
|
||||
void deserializeInternal(QDataStream &stream, quint32 version) override
|
||||
{
|
||||
Q_UNUSED(version); // only v0 exists currently
|
||||
|
||||
stream >> newResponse;
|
||||
THROW_IF_BAD(stream);
|
||||
|
||||
stream >> stopped;
|
||||
THROW_IF_BAD(stream);
|
||||
|
||||
stream >> rating;
|
||||
THROW_IF_BAD(stream);
|
||||
}
|
||||
|
||||
public:
|
||||
QString newResponse;
|
||||
bool stopped = false;
|
||||
MessageRating rating = MessageRating::Unrated;
|
||||
|
||||
private:
|
||||
ChatModel *m_model;
|
||||
};
|
||||
Q_DECLARE_METATYPE(ResponseContent)
|
||||
@@ -49,6 +49,7 @@ static const QVariantMap basicDefaults {
|
||||
{ "lastVersionStarted", "" },
|
||||
{ "networkPort", 4891, },
|
||||
{ "saveChatsContext", false },
|
||||
{ "systemTray", false },
|
||||
{ "serverChat", false },
|
||||
{ "userDefaultModel", "Application default" },
|
||||
{ "suggestionMode", QVariant::fromValue(SuggestionMode::LocalDocsOnly) },
|
||||
@@ -206,6 +207,7 @@ void MySettings::restoreApplicationDefaults()
|
||||
setDevice(defaults::device);
|
||||
setThreadCount(defaults::threadCount);
|
||||
setSaveChatsContext(basicDefaults.value("saveChatsContext").toBool());
|
||||
setSystemTray(basicDefaults.value("saveTrayContext").toBool());
|
||||
setServerChat(basicDefaults.value("serverChat").toBool());
|
||||
setNetworkPort(basicDefaults.value("networkPort").toInt());
|
||||
setModelPath(defaultLocalModelsPath());
|
||||
@@ -444,6 +446,7 @@ void MySettings::setThreadCount(int value)
|
||||
}
|
||||
|
||||
bool MySettings::saveChatsContext() const { return getBasicSetting("saveChatsContext" ).toBool(); }
|
||||
bool MySettings::systemTray() const { return getBasicSetting("systemTray" ).toBool(); }
|
||||
bool MySettings::serverChat() const { return getBasicSetting("serverChat" ).toBool(); }
|
||||
int MySettings::networkPort() const { return getBasicSetting("networkPort" ).toInt(); }
|
||||
QString MySettings::userDefaultModel() const { return getBasicSetting("userDefaultModel" ).toString(); }
|
||||
@@ -462,6 +465,7 @@ FontSize MySettings::fontSize() const { return FontSize (getEnu
|
||||
SuggestionMode MySettings::suggestionMode() const { return SuggestionMode(getEnumSetting("suggestionMode", suggestionModeNames)); }
|
||||
|
||||
void MySettings::setSaveChatsContext(bool value) { setBasicSetting("saveChatsContext", value); }
|
||||
void MySettings::setSystemTray(bool value) { setBasicSetting("systemTray", value); }
|
||||
void MySettings::setServerChat(bool value) { setBasicSetting("serverChat", value); }
|
||||
void MySettings::setNetworkPort(int value) { setBasicSetting("networkPort", value); }
|
||||
void MySettings::setUserDefaultModel(const QString &value) { setBasicSetting("userDefaultModel", value); }
|
||||
|
||||
@@ -49,6 +49,7 @@ class MySettings : public QObject
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int threadCount READ threadCount WRITE setThreadCount NOTIFY threadCountChanged)
|
||||
Q_PROPERTY(bool saveChatsContext READ saveChatsContext WRITE setSaveChatsContext NOTIFY saveChatsContextChanged)
|
||||
Q_PROPERTY(bool systemTray READ systemTray WRITE setSystemTray NOTIFY systemTrayChanged)
|
||||
Q_PROPERTY(bool serverChat READ serverChat WRITE setServerChat NOTIFY serverChatChanged)
|
||||
Q_PROPERTY(QString modelPath READ modelPath WRITE setModelPath NOTIFY modelPathChanged)
|
||||
Q_PROPERTY(QString userDefaultModel READ userDefaultModel WRITE setUserDefaultModel NOTIFY userDefaultModelChanged)
|
||||
@@ -142,6 +143,8 @@ public:
|
||||
void setThreadCount(int value);
|
||||
bool saveChatsContext() const;
|
||||
void setSaveChatsContext(bool value);
|
||||
bool systemTray() const;
|
||||
void setSystemTray(bool value);
|
||||
bool serverChat() const;
|
||||
void setServerChat(bool value);
|
||||
QString modelPath();
|
||||
@@ -218,6 +221,7 @@ Q_SIGNALS:
|
||||
void suggestedFollowUpPromptChanged(const ModelInfo &info);
|
||||
void threadCountChanged();
|
||||
void saveChatsContextChanged();
|
||||
void systemTrayChanged();
|
||||
void serverChatChanged();
|
||||
void modelPathChanged();
|
||||
void userDefaultModelChanged();
|
||||
|
||||
@@ -15,7 +15,7 @@ add_test(NAME ChatPythonTests
|
||||
COMMAND ${Python3_EXECUTABLE} -m pytest --color=yes "${CMAKE_CURRENT_SOURCE_DIR}/python"
|
||||
)
|
||||
set_tests_properties(ChatPythonTests PROPERTIES
|
||||
ENVIRONMENT "CHAT_EXECUTABLE=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/chat"
|
||||
ENVIRONMENT "CHAT_EXECUTABLE=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/chat;TEST_MODEL_PATH=${TEST_MODEL_PATH}"
|
||||
TIMEOUT 60
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from subprocess import CalledProcessError
|
||||
from typing import Any, Iterator
|
||||
@@ -20,59 +22,82 @@ class Requestor:
|
||||
self.session = requests.Session()
|
||||
self.http_adapter = self.session.adapters['http://']
|
||||
|
||||
def get(self, path: str, *, wait: bool = False) -> Any:
|
||||
return self._request('GET', path, wait)
|
||||
def get(self, path: str, *, raise_for_status: bool = True, wait: bool = False) -> Any:
|
||||
return self._request('GET', path, raise_for_status=raise_for_status, wait=wait)
|
||||
|
||||
def _request(self, method: str, path: str, wait: bool) -> Any:
|
||||
def post(self, path: str, data: dict[str, Any] | None, *, raise_for_status: bool = True, wait: bool = False) -> Any:
|
||||
return self._request('POST', path, data, raise_for_status=raise_for_status, wait=wait)
|
||||
|
||||
def _request(
|
||||
self, method: str, path: str, data: dict[str, Any] | None = None, *, raise_for_status: bool, wait: bool,
|
||||
) -> Any:
|
||||
if wait:
|
||||
retry = Retry(total=None, connect=10, read=False, status=0, other=0, backoff_factor=.01)
|
||||
else:
|
||||
retry = Retry(total=False)
|
||||
self.http_adapter.max_retries = retry # type: ignore[attr-defined]
|
||||
|
||||
resp = self.session.request(method, f'http://localhost:4891/v1/{path}')
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
resp = self.session.request(method, f'http://localhost:4891/v1/{path}', json=data)
|
||||
if raise_for_status:
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
try:
|
||||
json_data = resp.json()
|
||||
except ValueError:
|
||||
json_data = None
|
||||
return resp.status_code, json_data
|
||||
|
||||
|
||||
request = Requestor()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chat_server_config() -> Iterator[dict[str, str]]:
|
||||
def create_chat_server_config(tmpdir: Path, model_copied: bool = False) -> dict[str, str]:
|
||||
xdg_confdir = tmpdir / 'config'
|
||||
app_confdir = xdg_confdir / 'nomic.ai'
|
||||
app_confdir.mkdir(parents=True)
|
||||
with open(app_confdir / 'GPT4All.ini', 'w') as conf:
|
||||
conf.write(textwrap.dedent(f"""\
|
||||
[General]
|
||||
serverChat=true
|
||||
|
||||
[download]
|
||||
lastVersionStarted={config.APP_VERSION}
|
||||
|
||||
[network]
|
||||
isActive=false
|
||||
usageStatsActive=false
|
||||
"""))
|
||||
|
||||
if model_copied:
|
||||
app_data_dir = tmpdir / 'share' / 'nomic.ai' / 'GPT4All'
|
||||
app_data_dir.mkdir(parents=True)
|
||||
local_env_file_path = Path(os.environ['TEST_MODEL_PATH'])
|
||||
shutil.copy(local_env_file_path, app_data_dir / local_env_file_path.name)
|
||||
|
||||
return dict(
|
||||
os.environ,
|
||||
XDG_CACHE_HOME=str(tmpdir / 'cache'),
|
||||
XDG_DATA_HOME=str(tmpdir / 'share'),
|
||||
XDG_CONFIG_HOME=str(xdg_confdir),
|
||||
APPIMAGE=str(tmpdir), # hack to bypass SingleApplication
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def prepare_chat_server(model_copied: bool = False) -> Iterator[dict[str, str]]:
|
||||
if os.name != 'posix' or sys.platform == 'darwin':
|
||||
pytest.skip('Need non-Apple Unix to use alternate config path')
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='gpt4all-test') as td:
|
||||
tmpdir = Path(td)
|
||||
xdg_confdir = tmpdir / 'config'
|
||||
app_confdir = xdg_confdir / 'nomic.ai'
|
||||
app_confdir.mkdir(parents=True)
|
||||
with open(app_confdir / 'GPT4All.ini', 'w') as conf:
|
||||
conf.write(textwrap.dedent(f"""\
|
||||
[General]
|
||||
serverChat=true
|
||||
|
||||
[download]
|
||||
lastVersionStarted={config.APP_VERSION}
|
||||
|
||||
[network]
|
||||
isActive=false
|
||||
usageStatsActive=false
|
||||
"""))
|
||||
yield dict(
|
||||
os.environ,
|
||||
XDG_CACHE_HOME=str(tmpdir / 'cache'),
|
||||
XDG_DATA_HOME=str(tmpdir / 'share'),
|
||||
XDG_CONFIG_HOME=str(xdg_confdir),
|
||||
APPIMAGE=str(tmpdir), # hack to bypass SingleApplication
|
||||
)
|
||||
config = create_chat_server_config(tmpdir, model_copied=model_copied)
|
||||
yield config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chat_server(chat_server_config: dict[str, str]) -> Iterator[None]:
|
||||
def start_chat_server(config: dict[str, str]) -> Iterator[None]:
|
||||
chat_executable = Path(os.environ['CHAT_EXECUTABLE']).absolute()
|
||||
with subprocess.Popen(chat_executable, env=chat_server_config) as process:
|
||||
with subprocess.Popen(chat_executable, env=config) as process:
|
||||
try:
|
||||
yield
|
||||
except:
|
||||
@@ -83,5 +108,161 @@ def chat_server(chat_server_config: dict[str, str]) -> Iterator[None]:
|
||||
raise CalledProcessError(retcode, process.args)
|
||||
|
||||
|
||||
def test_list_models_empty(chat_server: None) -> None:
|
||||
assert request.get('models', wait=True) == {'object': 'list', 'data': []}
|
||||
@pytest.fixture
|
||||
def chat_server() -> Iterator[None]:
|
||||
with prepare_chat_server(model_copied=False) as config:
|
||||
yield from start_chat_server(config)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def chat_server_with_model() -> Iterator[None]:
|
||||
with prepare_chat_server(model_copied=True) as config:
|
||||
yield from start_chat_server(config)
|
||||
|
||||
|
||||
def test_with_models_empty(chat_server: None) -> None:
|
||||
# non-sense endpoint
|
||||
status_code, response = request.get('foobarbaz', wait=True, raise_for_status=False)
|
||||
assert status_code == 404
|
||||
assert response is None
|
||||
|
||||
# empty model list
|
||||
response = request.get('models')
|
||||
assert response == {'object': 'list', 'data': []}
|
||||
|
||||
# empty model info
|
||||
response = request.get('models/foo')
|
||||
assert response == {}
|
||||
|
||||
# POST for model list
|
||||
status_code, response = request.post('models', data=None, raise_for_status=False)
|
||||
assert status_code == 405
|
||||
assert response == {'error': {
|
||||
'code': None,
|
||||
'message': 'Not allowed to POST on /v1/models. (HINT: Perhaps you meant to use a different HTTP method?)',
|
||||
'param': None,
|
||||
'type': 'invalid_request_error',
|
||||
}}
|
||||
|
||||
# POST for model info
|
||||
status_code, response = request.post('models/foo', data=None, raise_for_status=False)
|
||||
assert status_code == 405
|
||||
assert response == {'error': {
|
||||
'code': None,
|
||||
'message': 'Not allowed to POST on /v1/models/*. (HINT: Perhaps you meant to use a different HTTP method?)',
|
||||
'param': None,
|
||||
'type': 'invalid_request_error',
|
||||
}}
|
||||
|
||||
# GET for completions
|
||||
status_code, response = request.get('completions', raise_for_status=False)
|
||||
assert status_code == 405
|
||||
assert response == {'error': {
|
||||
'code': 'method_not_supported',
|
||||
'message': 'Only POST requests are accepted.',
|
||||
'param': None,
|
||||
'type': 'invalid_request_error',
|
||||
}}
|
||||
|
||||
# GET for chat completions
|
||||
status_code, response = request.get('chat/completions', raise_for_status=False)
|
||||
assert status_code == 405
|
||||
assert response == {'error': {
|
||||
'code': 'method_not_supported',
|
||||
'message': 'Only POST requests are accepted.',
|
||||
'param': None,
|
||||
'type': 'invalid_request_error',
|
||||
}}
|
||||
|
||||
|
||||
EXPECTED_MODEL_INFO = {
|
||||
'created': 0,
|
||||
'id': 'Llama 3.2 1B Instruct',
|
||||
'object': 'model',
|
||||
'owned_by': 'humanity',
|
||||
'parent': None,
|
||||
'permissions': [
|
||||
{
|
||||
'allow_create_engine': False,
|
||||
'allow_fine_tuning': False,
|
||||
'allow_logprobs': False,
|
||||
'allow_sampling': False,
|
||||
'allow_search_indices': False,
|
||||
'allow_view': True,
|
||||
'created': 0,
|
||||
'group': None,
|
||||
'id': 'placeholder',
|
||||
'is_blocking': False,
|
||||
'object': 'model_permission',
|
||||
'organization': '*',
|
||||
},
|
||||
],
|
||||
'root': 'Llama 3.2 1B Instruct',
|
||||
}
|
||||
|
||||
EXPECTED_COMPLETIONS_RESPONSE = {
|
||||
'choices': [
|
||||
{
|
||||
'finish_reason': 'stop',
|
||||
'index': 0,
|
||||
'logprobs': None,
|
||||
'references': None,
|
||||
'text': ' jumps over the lazy dog.',
|
||||
},
|
||||
],
|
||||
'id': 'placeholder',
|
||||
'model': 'Llama 3.2 1B Instruct',
|
||||
'object': 'text_completion',
|
||||
'usage': {
|
||||
'completion_tokens': 6,
|
||||
'prompt_tokens': 5,
|
||||
'total_tokens': 11,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_with_models(chat_server_with_model: None) -> None:
|
||||
response = request.get('models', wait=True)
|
||||
assert response == {
|
||||
'data': [EXPECTED_MODEL_INFO],
|
||||
'object': 'list',
|
||||
}
|
||||
|
||||
# Test the specific model endpoint
|
||||
response = request.get('models/Llama 3.2 1B Instruct')
|
||||
assert response == EXPECTED_MODEL_INFO
|
||||
|
||||
# Test the completions endpoint
|
||||
status_code, response = request.post('completions', data=None, raise_for_status=False)
|
||||
assert status_code == 400
|
||||
assert response == {'error': {
|
||||
'code': None,
|
||||
'message': 'error parsing request JSON: illegal value',
|
||||
'param': None,
|
||||
'type': 'invalid_request_error',
|
||||
}}
|
||||
|
||||
data = {
|
||||
'model': 'Llama 3.2 1B Instruct',
|
||||
'prompt': 'The quick brown fox',
|
||||
'temperature': 0,
|
||||
}
|
||||
|
||||
response = request.post('completions', data=data)
|
||||
assert len(response['choices']) == 1
|
||||
assert response['choices'][0].keys() == {'text', 'index', 'logprobs', 'references', 'finish_reason'}
|
||||
assert response['choices'][0]['text'] == ' jumps over the lazy dog.'
|
||||
assert 'created' in response
|
||||
response.pop('created') # Remove the dynamic field for comparison
|
||||
assert response == EXPECTED_COMPLETIONS_RESPONSE
|
||||
|
||||
|
||||
@pytest.mark.xfail(reason='Assertion failure in GPT4All. See nomic-ai/gpt4all#3133')
|
||||
def test_with_models_temperature(chat_server_with_model: None) -> None:
|
||||
data = {
|
||||
'model': 'Llama 3.2 1B Instruct',
|
||||
'prompt': 'The quick brown fox',
|
||||
'temperature': 0.5,
|
||||
}
|
||||
|
||||
request.post('completions', data=data, wait=True, raise_for_status=True)
|
||||
|
||||
Reference in New Issue
Block a user