diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c7d259a82e..40e18f7a6b3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,8 +16,11 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- When a search hits a file, the file icon of that entry is changed accordingly. [#11542](https://github.com/JabRef/jabref/pull/11542)
- We added an AI-based chat for entries with linked PDF files. [#11430](https://github.com/JabRef/jabref/pull/11430)
- We added an AI-based summarization possibility for entries with linked PDF files. [#11430](https://github.com/JabRef/jabref/pull/11430)
+- We added an AI section in JabRef's [preferences](https://docs.jabref.org/ai/preferences). [#11430](https://github.com/JabRef/jabref/pull/11430)
+- We added AI providers: OpenAI, Mistral AI, Hugging Face and Google. [#11430](https://github.com/JabRef/jabref/pull/11430), [#11736](https://github.com/JabRef/jabref/pull/11736)
+- We added AI providers: [Ollama](https://docs.jabref.org/ai/local-llm#step-by-step-guide-for-ollama) and GPT4All, which add the possibility to use local LLMs privately on your own device. [#11430](https://github.com/JabRef/jabref/pull/11430), [#11870](https://github.com/JabRef/jabref/issues/11870)
- We added support for selecting and using CSL Styles in JabRef's OpenOffice/LibreOffice integration for inserting bibliographic and in-text citations into a document. [#2146](https://github.com/JabRef/jabref/issues/2146), [#8893](https://github.com/JabRef/jabref/issues/8893)
-- We added Tools > New library based on references in PDF file... to create a new library based on the references section in a PDF file. [#11522](https://github.com/JabRef/jabref/pull/11522)
+- We added "Tools > New library based on references in PDF file" ... to create a new library based on the references section in a PDF file. [#11522](https://github.com/JabRef/jabref/pull/11522)
- When converting the references section of a paper (PDF file), more than the last page is treated. [#11522](https://github.com/JabRef/jabref/pull/11522)
- Added the functionality to invoke offline reference parsing explicitly. [#11565](https://github.com/JabRef/jabref/pull/11565)
- The dialog for [adding an entry using reference text](https://docs.jabref.org/collect/newentryfromplaintext) is now filled with the clipboard contents as default. [#11565](https://github.com/JabRef/jabref/pull/11565)
@@ -66,6 +69,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We changed instances of 'Search Selected' to 'Search Pre-configured' in Web Search Preferences UI. [#11871](https://github.com/JabRef/jabref/pull/11871)
- We added a new CSS style class `main-table` for the main table. [#11881](https://github.com/JabRef/jabref/pull/11881)
- When renaming a file, the old extension is now used if there is none provided in the new name. [#11903](https://github.com/JabRef/jabref/issues/11903)
+- Added minimum window sizing for windows dedicated to creating new entries [#11944](https://github.com/JabRef/jabref/issues/11944)
- We changed the name of the library-based file directory from 'General File Directory' to 'Library-specific File Directory' per issue [#571](https://github.com/koppor/jabref/issues/571)
- The CitationKey column is now a default shown column for the entry table. [#10510](https://github.com/JabRef/jabref/issues/10510)
diff --git a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml
index 2f439beac31..a4f4da47cc2 100644
--- a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml
+++ b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.fxml
@@ -66,6 +66,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java
index 12dad69d580..4ef78da8c36 100644
--- a/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java
+++ b/src/main/java/org/jabref/gui/ai/components/privacynotice/PrivacyNoticeComponent.java
@@ -25,6 +25,7 @@ public class PrivacyNoticeComponent extends ScrollPane {
@FXML private TextFlow mistralAiPrivacyTextFlow;
@FXML private TextFlow geminiPrivacyTextFlow;
@FXML private TextFlow huggingFacePrivacyTextFlow;
+ @FXML private TextFlow gpt4AllTextFlow;
@FXML private Text embeddingModelText;
private final AiPreferences aiPreferences;
@@ -49,6 +50,7 @@ private void initialize() {
initPrivacyHyperlink(mistralAiPrivacyTextFlow, AiProvider.MISTRAL_AI);
initPrivacyHyperlink(geminiPrivacyTextFlow, AiProvider.GEMINI);
initPrivacyHyperlink(huggingFacePrivacyTextFlow, AiProvider.HUGGING_FACE);
+ initPrivacyHyperlink(gpt4AllTextFlow, AiProvider.GPT4ALL);
String newEmbeddingModelText = embeddingModelText.getText().replaceAll("%0", aiPreferences.getEmbeddingModel().sizeInfo());
embeddingModelText.setText(newEmbeddingModelText);
diff --git a/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java b/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java
index 8884561f2a7..a66e407522c 100644
--- a/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java
+++ b/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java
@@ -16,6 +16,7 @@
import javafx.scene.control.Tooltip;
import javafx.scene.layout.FlowPane;
import javafx.stage.Screen;
+import javafx.stage.Stage;
import org.jabref.gui.DialogService;
import org.jabref.gui.LibraryTab;
@@ -87,6 +88,10 @@ public EntryTypeView(LibraryTab libraryTab, DialogService dialogService, GuiPref
.load()
.setAsDialogPane(this);
+ Stage stage = (Stage) getDialogPane().getScene().getWindow();
+ stage.setMinHeight(400);
+ stage.setMinWidth(500);
+
ControlHelper.setAction(generateButton, this.getDialogPane(), event -> viewModel.runFetcherWorker());
setOnCloseRequest(e -> viewModel.cancelFetcherWorker());
diff --git a/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java
index a72b8c66d3a..f2b21abd906 100644
--- a/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java
+++ b/src/main/java/org/jabref/gui/plaincitationparser/PlainCitationParserDialog.java
@@ -9,6 +9,7 @@
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextArea;
import javafx.scene.control.Tooltip;
+import javafx.stage.Stage;
import org.jabref.gui.ClipBoardManager;
import org.jabref.gui.DialogService;
@@ -53,6 +54,10 @@ public PlainCitationParserDialog() {
.setAsDialogPane(this);
this.setTitle(Localization.lang("Plain Citations Parser"));
+
+ Stage stage = (Stage) getDialogPane().getScene().getWindow();
+ stage.setMinHeight(550);
+ stage.setMinWidth(500);
}
@FXML
diff --git a/src/main/java/org/jabref/gui/preferences/ai/AiTab.java b/src/main/java/org/jabref/gui/preferences/ai/AiTab.java
index efa406a1d27..3360e3a5b4a 100644
--- a/src/main/java/org/jabref/gui/preferences/ai/AiTab.java
+++ b/src/main/java/org/jabref/gui/preferences/ai/AiTab.java
@@ -1,6 +1,7 @@
package org.jabref.gui.preferences.ai;
import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@@ -28,6 +29,7 @@
public class AiTab extends AbstractPreferenceTabView implements PreferencesTab {
private static final String HUGGING_FACE_CHAT_MODEL_PROMPT = "TinyLlama/TinyLlama_v1.1 (or any other model name)";
+ private static final String GPT_4_ALL_CHAT_MODEL_PROMPT = "Phi-3.1-mini (or any other local model name from GPT4All)";
@FXML private CheckBox enableAi;
@FXML private CheckBox autoGenerateEmbeddings;
@@ -87,10 +89,19 @@ public void initialize() {
if (newValue == AiProvider.HUGGING_FACE) {
chatModelComboBox.setPromptText(HUGGING_FACE_CHAT_MODEL_PROMPT);
}
+ if (newValue == AiProvider.GPT4ALL) {
+ chatModelComboBox.setPromptText(GPT_4_ALL_CHAT_MODEL_PROMPT);
+ }
});
apiKeyTextField.textProperty().bindBidirectional(viewModel.apiKeyProperty());
- apiKeyTextField.disableProperty().bind(viewModel.disableBasicSettingsProperty());
+ // Disable if GPT4ALL is selected
+ apiKeyTextField.disableProperty().bind(
+ Bindings.or(
+ viewModel.disableBasicSettingsProperty(),
+ aiProviderComboBox.valueProperty().isEqualTo(AiProvider.GPT4ALL)
+ )
+ );
customizeExpertSettingsCheckbox.selectedProperty().bindBidirectional(viewModel.customizeExpertSettingsProperty());
customizeExpertSettingsCheckbox.disableProperty().bind(viewModel.disableBasicSettingsProperty());
diff --git a/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java b/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java
index c1c4ef5f6a5..b8871f235e4 100644
--- a/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java
+++ b/src/main/java/org/jabref/gui/preferences/ai/AiTabViewModel.java
@@ -54,6 +54,7 @@ public class AiTabViewModel implements PreferenceTabViewModel {
private final StringProperty mistralAiChatModel = new SimpleStringProperty();
private final StringProperty geminiChatModel = new SimpleStringProperty();
private final StringProperty huggingFaceChatModel = new SimpleStringProperty();
+ private final StringProperty gpt4AllChatModel = new SimpleStringProperty();
private final StringProperty currentApiKey = new SimpleStringProperty();
@@ -61,6 +62,7 @@ public class AiTabViewModel implements PreferenceTabViewModel {
private final StringProperty mistralAiApiKey = new SimpleStringProperty();
private final StringProperty geminiAiApiKey = new SimpleStringProperty();
private final StringProperty huggingFaceApiKey = new SimpleStringProperty();
+ private final StringProperty gpt4AllApiKey = new SimpleStringProperty();
private final BooleanProperty customizeExpertSettings = new SimpleBooleanProperty();
@@ -75,6 +77,7 @@ public class AiTabViewModel implements PreferenceTabViewModel {
private final StringProperty mistralAiApiBaseUrl = new SimpleStringProperty();
private final StringProperty geminiApiBaseUrl = new SimpleStringProperty();
private final StringProperty huggingFaceApiBaseUrl = new SimpleStringProperty();
+ private final StringProperty gpt4AllApiBaseUrl = new SimpleStringProperty();
private final StringProperty instruction = new SimpleStringProperty();
private final StringProperty temperature = new SimpleStringProperty();
@@ -150,6 +153,11 @@ public AiTabViewModel(CliPreferences preferences) {
huggingFaceApiKey.set(currentApiKey.get());
huggingFaceApiBaseUrl.set(currentApiBaseUrl.get());
}
+ case GPT4ALL-> {
+ gpt4AllChatModel.set(oldChatModel);
+ gpt4AllApiKey.set(currentApiKey.get());
+ gpt4AllApiBaseUrl.set(currentApiBaseUrl.get());
+ }
}
}
@@ -174,6 +182,11 @@ public AiTabViewModel(CliPreferences preferences) {
currentApiKey.set(huggingFaceApiKey.get());
currentApiBaseUrl.set(huggingFaceApiBaseUrl.get());
}
+ case GPT4ALL -> {
+ currentChatModel.set(gpt4AllChatModel.get());
+ currentApiKey.set(gpt4AllApiKey.get());
+ currentApiBaseUrl.set(gpt4AllApiBaseUrl.get());
+ }
}
});
@@ -183,6 +196,7 @@ public AiTabViewModel(CliPreferences preferences) {
case MISTRAL_AI -> mistralAiChatModel.set(newValue);
case GEMINI -> geminiChatModel.set(newValue);
case HUGGING_FACE -> huggingFaceChatModel.set(newValue);
+ case GPT4ALL -> gpt4AllChatModel.set(newValue);
}
contextWindowSize.set(AiDefaultPreferences.getContextWindowSize(selectedAiProvider.get(), newValue));
@@ -194,6 +208,7 @@ public AiTabViewModel(CliPreferences preferences) {
case MISTRAL_AI -> mistralAiApiKey.set(newValue);
case GEMINI -> geminiAiApiKey.set(newValue);
case HUGGING_FACE -> huggingFaceApiKey.set(newValue);
+ case GPT4ALL -> gpt4AllApiKey.set(newValue);
}
});
@@ -203,6 +218,7 @@ public AiTabViewModel(CliPreferences preferences) {
case MISTRAL_AI -> mistralAiApiBaseUrl.set(newValue);
case GEMINI -> geminiApiBaseUrl.set(newValue);
case HUGGING_FACE -> huggingFaceApiBaseUrl.set(newValue);
+ case GPT4ALL -> gpt4AllApiBaseUrl.set(newValue);
}
});
@@ -279,16 +295,19 @@ public void setValues() {
mistralAiApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.MISTRAL_AI));
geminiAiApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.GEMINI));
huggingFaceApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.HUGGING_FACE));
+ gpt4AllApiKey.setValue(aiPreferences.getApiKeyForAiProvider(AiProvider.GPT4ALL));
openAiApiBaseUrl.setValue(aiPreferences.getOpenAiApiBaseUrl());
mistralAiApiBaseUrl.setValue(aiPreferences.getMistralAiApiBaseUrl());
geminiApiBaseUrl.setValue(aiPreferences.getGeminiApiBaseUrl());
huggingFaceApiBaseUrl.setValue(aiPreferences.getHuggingFaceApiBaseUrl());
+ gpt4AllApiBaseUrl.setValue(aiPreferences.getGpt4AllApiBaseUrl());
openAiChatModel.setValue(aiPreferences.getOpenAiChatModel());
mistralAiChatModel.setValue(aiPreferences.getMistralAiChatModel());
geminiChatModel.setValue(aiPreferences.getGeminiChatModel());
huggingFaceChatModel.setValue(aiPreferences.getHuggingFaceChatModel());
+ gpt4AllChatModel.setValue(aiPreferences.getGpt4AllChatModel());
enableAi.setValue(aiPreferences.getEnableAi());
autoGenerateSummaries.setValue(aiPreferences.getAutoGenerateSummaries());
@@ -320,11 +339,13 @@ public void storeSettings() {
aiPreferences.setMistralAiChatModel(mistralAiChatModel.get() == null ? "" : mistralAiChatModel.get());
aiPreferences.setGeminiChatModel(geminiChatModel.get() == null ? "" : geminiChatModel.get());
aiPreferences.setHuggingFaceChatModel(huggingFaceChatModel.get() == null ? "" : huggingFaceChatModel.get());
+ aiPreferences.setGpt4AllChatModel(gpt4AllChatModel.get() == null ? "" : gpt4AllChatModel.get());
aiPreferences.storeAiApiKeyInKeyring(AiProvider.OPEN_AI, openAiApiKey.get() == null ? "" : openAiApiKey.get());
aiPreferences.storeAiApiKeyInKeyring(AiProvider.MISTRAL_AI, mistralAiApiKey.get() == null ? "" : mistralAiApiKey.get());
aiPreferences.storeAiApiKeyInKeyring(AiProvider.GEMINI, geminiAiApiKey.get() == null ? "" : geminiAiApiKey.get());
aiPreferences.storeAiApiKeyInKeyring(AiProvider.HUGGING_FACE, huggingFaceApiKey.get() == null ? "" : huggingFaceApiKey.get());
+ aiPreferences.storeAiApiKeyInKeyring(AiProvider.GPT4ALL, gpt4AllApiKey.get() == null ? "" : gpt4AllApiKey.get());
// We notify in all cases without a real check if something was changed
aiPreferences.apiKeyUpdated();
@@ -336,6 +357,7 @@ public void storeSettings() {
aiPreferences.setMistralAiApiBaseUrl(mistralAiApiBaseUrl.get() == null ? "" : mistralAiApiBaseUrl.get());
aiPreferences.setGeminiApiBaseUrl(geminiApiBaseUrl.get() == null ? "" : geminiApiBaseUrl.get());
aiPreferences.setHuggingFaceApiBaseUrl(huggingFaceApiBaseUrl.get() == null ? "" : huggingFaceApiBaseUrl.get());
+ aiPreferences.setGpt4AllApiBaseUrl(gpt4AllApiBaseUrl.get() == null ? "" : gpt4AllApiBaseUrl.get());
aiPreferences.setInstruction(instruction.get());
// We already check the correctness of temperature and RAG minimum score in validators, so we don't need to check it here.
diff --git a/src/main/java/org/jabref/gui/push/PushToTeXworks.java b/src/main/java/org/jabref/gui/push/PushToTeXworks.java
index 6459d848c13..87c3a3b9931 100644
--- a/src/main/java/org/jabref/gui/push/PushToTeXworks.java
+++ b/src/main/java/org/jabref/gui/push/PushToTeXworks.java
@@ -38,7 +38,6 @@ protected String[] getCommandLine(String keyString) {
@Override
protected String[] jumpToLineCommandlineArguments(Path fileName, int line, int column) {
- // No command known to jump to a specific line
- return new String[] {commandPath, fileName.toString()};
+ return new String[] {commandPath, "--position=\"%s\"".formatted(line), fileName.toString()};
}
}
diff --git a/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java b/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java
index e83c1a084b5..3cdbd9bfda8 100644
--- a/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java
+++ b/src/main/java/org/jabref/logic/ai/AiDefaultPreferences.java
@@ -23,7 +23,9 @@ public enum PredefinedChatModel {
GEMINI_1_5_PRO(AiProvider.GEMINI, "gemini-1.5-pro", 2097152),
GEMINI_1_0_PRO(AiProvider.GEMINI, "gemini-1.0-pro", 32000),
// Dummy variant for Hugging Face models.
- HUGGING_FACE(AiProvider.HUGGING_FACE, "", 0);
+ // Blank entry used for cases where the model name is not specified.
+ BLANK_HUGGING_FACE(AiProvider.HUGGING_FACE, "", 0),
+ BLANK_GPT4ALL(AiProvider.GPT4ALL, "", 0);
private final AiProvider aiProvider;
private final String name;
@@ -62,7 +64,8 @@ public String toString() {
AiProvider.OPEN_AI, PredefinedChatModel.GPT_4O_MINI,
AiProvider.MISTRAL_AI, PredefinedChatModel.OPEN_MIXTRAL_8X22B,
AiProvider.GEMINI, PredefinedChatModel.GEMINI_1_5_FLASH,
- AiProvider.HUGGING_FACE, PredefinedChatModel.HUGGING_FACE
+ AiProvider.HUGGING_FACE, PredefinedChatModel.BLANK_HUGGING_FACE,
+ AiProvider.GPT4ALL, PredefinedChatModel.BLANK_GPT4ALL
);
public static final boolean CUSTOMIZE_SETTINGS = false;
diff --git a/src/main/java/org/jabref/logic/ai/AiPreferences.java b/src/main/java/org/jabref/logic/ai/AiPreferences.java
index 3a07a02e70c..48b10812338 100644
--- a/src/main/java/org/jabref/logic/ai/AiPreferences.java
+++ b/src/main/java/org/jabref/logic/ai/AiPreferences.java
@@ -40,6 +40,7 @@ public class AiPreferences {
private final StringProperty mistralAiChatModel;
private final StringProperty geminiChatModel;
private final StringProperty huggingFaceChatModel;
+ private final StringProperty gpt4AllChatModel;
private final BooleanProperty customizeExpertSettings;
@@ -47,6 +48,7 @@ public class AiPreferences {
private final StringProperty mistralAiApiBaseUrl;
private final StringProperty geminiApiBaseUrl;
private final StringProperty huggingFaceApiBaseUrl;
+ private final StringProperty gpt4AllApiBaseUrl;
private final ObjectProperty embeddingModel;
private final StringProperty instruction;
@@ -67,11 +69,13 @@ public AiPreferences(boolean enableAi,
String mistralAiChatModel,
String geminiChatModel,
String huggingFaceChatModel,
+ String gpt4AllModel,
boolean customizeExpertSettings,
String openAiApiBaseUrl,
String mistralAiApiBaseUrl,
String geminiApiBaseUrl,
String huggingFaceApiBaseUrl,
+ String gpt4AllApiBaseUrl,
EmbeddingModel embeddingModel,
String instruction,
double temperature,
@@ -91,6 +95,7 @@ public AiPreferences(boolean enableAi,
this.mistralAiChatModel = new SimpleStringProperty(mistralAiChatModel);
this.geminiChatModel = new SimpleStringProperty(geminiChatModel);
this.huggingFaceChatModel = new SimpleStringProperty(huggingFaceChatModel);
+ this.gpt4AllChatModel = new SimpleStringProperty(gpt4AllModel);
this.customizeExpertSettings = new SimpleBooleanProperty(customizeExpertSettings);
@@ -98,6 +103,7 @@ public AiPreferences(boolean enableAi,
this.mistralAiApiBaseUrl = new SimpleStringProperty(mistralAiApiBaseUrl);
this.geminiApiBaseUrl = new SimpleStringProperty(geminiApiBaseUrl);
this.huggingFaceApiBaseUrl = new SimpleStringProperty(huggingFaceApiBaseUrl);
+ this.gpt4AllApiBaseUrl = new SimpleStringProperty(gpt4AllApiBaseUrl);
this.embeddingModel = new SimpleObjectProperty<>(embeddingModel);
this.instruction = new SimpleStringProperty(instruction);
@@ -233,6 +239,18 @@ public void setHuggingFaceChatModel(String huggingFaceChatModel) {
this.huggingFaceChatModel.set(huggingFaceChatModel);
}
+ public StringProperty gpt4AllChatModelProperty() {
+ return gpt4AllChatModel;
+ }
+
+ public String getGpt4AllChatModel() {
+ return huggingFaceChatModel.get();
+ }
+
+ public void setGpt4AllChatModel(String gpt4AllChatModel) {
+ this.gpt4AllChatModel.set(gpt4AllChatModel);
+ }
+
public BooleanProperty customizeExpertSettingsProperty() {
return customizeExpertSettings;
}
@@ -309,6 +327,18 @@ public void setHuggingFaceApiBaseUrl(String huggingFaceApiBaseUrl) {
this.huggingFaceApiBaseUrl.set(huggingFaceApiBaseUrl);
}
+ public StringProperty gpt4AllApiBaseUrlProperty() {
+ return gpt4AllApiBaseUrl;
+ }
+
+ public String getGpt4AllApiBaseUrl() {
+ return gpt4AllApiBaseUrl.get();
+ }
+
+ public void setGpt4AllApiBaseUrl(String gpt4AllApiBaseUrl) {
+ this.gpt4AllApiBaseUrl.set(gpt4AllApiBaseUrl);
+ }
+
public StringProperty instructionProperty() {
return instruction;
}
@@ -354,6 +384,7 @@ public int getContextWindowSize() {
case MISTRAL_AI -> AiDefaultPreferences.getContextWindowSize(AiProvider.MISTRAL_AI, mistralAiChatModel.get());
case HUGGING_FACE -> AiDefaultPreferences.getContextWindowSize(AiProvider.HUGGING_FACE, huggingFaceChatModel.get());
case GEMINI -> AiDefaultPreferences.getContextWindowSize(AiProvider.GEMINI, geminiChatModel.get());
+ case GPT4ALL -> AiDefaultPreferences.getContextWindowSize(AiProvider.GPT4ALL, gpt4AllChatModel.get());
};
}
}
@@ -481,6 +512,8 @@ public String getSelectedChatModel() {
huggingFaceChatModel.get();
case GEMINI ->
geminiChatModel.get();
+ case GPT4ALL ->
+ gpt4AllChatModel.get();
};
}
@@ -495,6 +528,8 @@ public String getSelectedApiBaseUrl() {
huggingFaceApiBaseUrl.get();
case GEMINI ->
geminiApiBaseUrl.get();
+ case GPT4ALL ->
+ gpt4AllApiBaseUrl.get();
};
} else {
return aiProvider.get().getApiUrl();
diff --git a/src/main/java/org/jabref/logic/ai/chatting/model/Gpt4AllModel.java b/src/main/java/org/jabref/logic/ai/chatting/model/Gpt4AllModel.java
new file mode 100644
index 00000000000..20c26951f69
--- /dev/null
+++ b/src/main/java/org/jabref/logic/ai/chatting/model/Gpt4AllModel.java
@@ -0,0 +1,148 @@
+package org.jabref.logic.ai.chatting.model;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.jabref.logic.ai.AiPreferences;
+
+import com.google.gson.Gson;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.ToolExecutionResultMessage;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.model.chat.ChatLanguageModel;
+import dev.langchain4j.model.output.FinishReason;
+import dev.langchain4j.model.output.Response;
+import dev.langchain4j.model.output.TokenUsage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Gpt4AllModel implements ChatLanguageModel {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Gpt4AllModel.class);
+
+ private final AiPreferences aiPreferences;
+ private final HttpClient httpClient;
+ private final Gson gson = new Gson();
+
+ public Gpt4AllModel(AiPreferences aiPreferences, HttpClient httpClient) {
+ this.aiPreferences = aiPreferences;
+ this.httpClient = httpClient;
+ }
+
+ @Override
+ public Response generate(List list) {
+ LOGGER.debug("Generating response from Gpt4All model with {} messages: {}", list.size(), list);
+
+ List messages = list.stream()
+ .map(chatMessage -> switch (chatMessage) {
+ case AiMessage aiMessage -> new Message("assistant", aiMessage.text());
+ case SystemMessage systemMessage -> new Message("system", systemMessage.text());
+ case ToolExecutionResultMessage toolExecutionResultMessage -> new Message("tool", toolExecutionResultMessage.text());
+ case UserMessage userMessage -> new Message("user", userMessage.singleText());
+ default -> throw new IllegalStateException("Unknown ChatMessage type: " + chatMessage);
+ }).collect(Collectors.toList());
+
+ TextGenerationRequest request = TextGenerationRequest
+ .builder()
+ .model(aiPreferences.getSelectedChatModel())
+ .messages(messages)
+ .temperature(aiPreferences.getTemperature())
+ .max_tokens(2048)
+ .build();
+
+ try {
+ String requestBody = gson.toJson(request);
+ String baseUrl = aiPreferences.getSelectedApiBaseUrl();
+ String fullUrl = baseUrl.endsWith("/") ? baseUrl + "chat/completions" : baseUrl + "/chat/completions";
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .uri(URI.create(fullUrl))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(requestBody))
+ .timeout(Duration.ofSeconds(60))
+ .build();
+
+ HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
+ LOGGER.info("Gpt4All response: {}", response.body());
+
+ TextGenerationResponse textGenerationResponse = gson.fromJson(response.body(), TextGenerationResponse.class);
+ if (textGenerationResponse.choices() == null || textGenerationResponse.choices().isEmpty()) {
+ throw new IllegalArgumentException("No choices returned in the response");
+ }
+
+ String generatedText = textGenerationResponse.choices().getFirst().message().content();
+ if (generatedText == null || generatedText.isEmpty()) {
+ throw new IllegalArgumentException("Generated text is null or empty");
+ }
+
+ // Note: We do not check the token usage and finish reason here.
+ // This class is not a complete implementation of langchain4j's ChatLanguageModel.
+ // We only implemented the functionality we specifically need.
+ return new Response<>(new AiMessage(generatedText), new TokenUsage(0, 0), FinishReason.OTHER);
+ } catch (Exception e) {
+ LOGGER.error("Error generating message from Gpt4All", e);
+ throw new RuntimeException("Failed to generate AI message", e);
+ }
+ }
+
+ private static class TextGenerationRequest {
+ protected final String model;
+ protected final List messages;
+ protected final Double temperature;
+ protected final Integer max_tokens;
+
+ private TextGenerationRequest(Builder builder) {
+ this.model = builder.model;
+ this.messages = builder.messages;
+ this.temperature = builder.temperature;
+ this.max_tokens = builder.max_tokens;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ private String model;
+ private List messages;
+ private Double temperature;
+ private Integer max_tokens;
+
+ public Builder model(String model) {
+ this.model = model;
+ return this;
+ }
+
+ public Builder messages(List messages) {
+ this.messages = messages;
+ return this;
+ }
+
+ public Builder temperature(Double temperature) {
+ this.temperature = temperature;
+ return this;
+ }
+
+ public Builder max_tokens(Integer max_tokens) {
+ this.max_tokens = max_tokens;
+ return this;
+ }
+
+ public TextGenerationRequest build() {
+ return new TextGenerationRequest(this);
+ }
+ }
+ }
+
+ private record TextGenerationResponse(List choices) { }
+
+ private record Choice(Message message) { }
+
+ private record Message(String role, String content) { }
+}
diff --git a/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java b/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java
index 92fc11df484..8445e15347d 100644
--- a/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java
+++ b/src/main/java/org/jabref/logic/ai/chatting/model/JabRefChatLanguageModel.java
@@ -10,6 +10,7 @@
import org.jabref.logic.ai.AiPreferences;
import org.jabref.logic.ai.chatting.AiChatLogic;
import org.jabref.logic.l10n.Localization;
+import org.jabref.model.ai.AiProvider;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import dev.langchain4j.data.message.AiMessage;
@@ -54,7 +55,7 @@ public JabRefChatLanguageModel(AiPreferences aiPreferences) {
*/
private void rebuild() {
String apiKey = aiPreferences.getApiKeyForAiProvider(aiPreferences.getAiProvider());
- if (!aiPreferences.getEnableAi() || apiKey.isEmpty()) {
+ if (!aiPreferences.getEnableAi() || (apiKey.isEmpty() && aiPreferences.getAiProvider() != AiProvider.GPT4ALL)) {
langchainChatModel = Optional.empty();
return;
}
@@ -64,6 +65,10 @@ private void rebuild() {
langchainChatModel = Optional.of(new JvmOpenAiChatLanguageModel(aiPreferences, httpClient));
}
+ case GPT4ALL-> {
+ langchainChatModel = Optional.of(new Gpt4AllModel(aiPreferences, httpClient));
+ }
+
case MISTRAL_AI -> {
langchainChatModel = Optional.of(MistralAiChatModel
.builder()
@@ -129,7 +134,7 @@ public Response generate(List list) {
if (langchainChatModel.isEmpty()) {
if (!aiPreferences.getEnableAi()) {
throw new RuntimeException(Localization.lang("In order to use AI chat, you need to enable chatting with attached PDF files in JabRef preferences (AI tab)."));
- } else if (aiPreferences.getApiKeyForAiProvider(aiPreferences.getAiProvider()).isEmpty()) {
+ } else if (aiPreferences.getApiKeyForAiProvider(aiPreferences.getAiProvider()).isEmpty() && aiPreferences.getAiProvider() != AiProvider.GPT4ALL) {
throw new RuntimeException(Localization.lang("In order to use AI chat, set an API key inside JabRef preferences (AI tab)."));
} else {
rebuild();
diff --git a/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java b/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java
index f3ecebe41d9..27760c02f70 100644
--- a/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java
+++ b/src/main/java/org/jabref/logic/preferences/JabRefCliPreferences.java
@@ -356,12 +356,14 @@ public class JabRefCliPreferences implements CliPreferences {
private static final String AI_MISTRAL_AI_CHAT_MODEL = "aiMistralAiChatModel";
private static final String AI_GEMINI_CHAT_MODEL = "aiGeminiChatModel";
private static final String AI_HUGGING_FACE_CHAT_MODEL = "aiHuggingFaceChatModel";
+ private static final String AI_GPT_4_ALL_MODEL = "aiGpt4AllChatModel";
private static final String AI_CUSTOMIZE_SETTINGS = "aiCustomizeSettings";
private static final String AI_EMBEDDING_MODEL = "aiEmbeddingModel";
private static final String AI_OPEN_AI_API_BASE_URL = "aiOpenAiApiBaseUrl";
private static final String AI_MISTRAL_AI_API_BASE_URL = "aiMistralAiApiBaseUrl";
private static final String AI_GEMINI_API_BASE_URL = "aiGeminiApiBaseUrl";
private static final String AI_HUGGING_FACE_API_BASE_URL = "aiHuggingFaceApiBaseUrl";
+ private static final String AI_GPT_4_ALL_API_BASE_URL = "aiGpt4AllApiBaseUrl";
private static final String AI_SYSTEM_MESSAGE = "aiSystemMessage";
private static final String AI_TEMPERATURE = "aiTemperature";
private static final String AI_CONTEXT_WINDOW_SIZE = "aiMessageWindowSize";
@@ -641,12 +643,14 @@ protected JabRefCliPreferences() {
defaults.put(AI_MISTRAL_AI_CHAT_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.MISTRAL_AI).getName());
defaults.put(AI_GEMINI_CHAT_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.GEMINI).getName());
defaults.put(AI_HUGGING_FACE_CHAT_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.HUGGING_FACE).getName());
+ defaults.put(AI_GPT_4_ALL_MODEL, AiDefaultPreferences.CHAT_MODELS.get(AiProvider.GPT4ALL).getName());
defaults.put(AI_CUSTOMIZE_SETTINGS, AiDefaultPreferences.CUSTOMIZE_SETTINGS);
defaults.put(AI_EMBEDDING_MODEL, AiDefaultPreferences.EMBEDDING_MODEL.name());
defaults.put(AI_OPEN_AI_API_BASE_URL, AiProvider.OPEN_AI.getApiUrl());
defaults.put(AI_MISTRAL_AI_API_BASE_URL, AiProvider.MISTRAL_AI.getApiUrl());
defaults.put(AI_GEMINI_API_BASE_URL, AiProvider.GEMINI.getApiUrl());
defaults.put(AI_HUGGING_FACE_API_BASE_URL, AiProvider.HUGGING_FACE.getApiUrl());
+ defaults.put(AI_GPT_4_ALL_API_BASE_URL, AiProvider.GPT4ALL.getApiUrl());
defaults.put(AI_SYSTEM_MESSAGE, AiDefaultPreferences.SYSTEM_MESSAGE);
defaults.put(AI_TEMPERATURE, AiDefaultPreferences.TEMPERATURE);
defaults.put(AI_CONTEXT_WINDOW_SIZE, AiDefaultPreferences.getContextWindowSize(AiDefaultPreferences.PROVIDER, AiDefaultPreferences.CHAT_MODELS.get(AiDefaultPreferences.PROVIDER).getName()));
@@ -1835,11 +1839,13 @@ public AiPreferences getAiPreferences() {
get(AI_MISTRAL_AI_CHAT_MODEL),
get(AI_GEMINI_CHAT_MODEL),
get(AI_HUGGING_FACE_CHAT_MODEL),
+ get(AI_GPT_4_ALL_MODEL),
getBoolean(AI_CUSTOMIZE_SETTINGS),
get(AI_OPEN_AI_API_BASE_URL),
get(AI_MISTRAL_AI_API_BASE_URL),
get(AI_GEMINI_API_BASE_URL),
get(AI_HUGGING_FACE_API_BASE_URL),
+ get(AI_GPT_4_ALL_API_BASE_URL),
EmbeddingModel.valueOf(get(AI_EMBEDDING_MODEL)),
get(AI_SYSTEM_MESSAGE),
getDouble(AI_TEMPERATURE),
@@ -1859,6 +1865,7 @@ public AiPreferences getAiPreferences() {
EasyBind.listen(aiPreferences.mistralAiChatModelProperty(), (obs, oldValue, newValue) -> put(AI_MISTRAL_AI_CHAT_MODEL, newValue));
EasyBind.listen(aiPreferences.geminiChatModelProperty(), (obs, oldValue, newValue) -> put(AI_GEMINI_CHAT_MODEL, newValue));
EasyBind.listen(aiPreferences.huggingFaceChatModelProperty(), (obs, oldValue, newValue) -> put(AI_HUGGING_FACE_CHAT_MODEL, newValue));
+ EasyBind.listen(aiPreferences.gpt4AllChatModelProperty(), (obs, oldValue, newValue) -> put(AI_GPT_4_ALL_MODEL, newValue));
EasyBind.listen(aiPreferences.customizeExpertSettingsProperty(), (obs, oldValue, newValue) -> putBoolean(AI_CUSTOMIZE_SETTINGS, newValue));
@@ -1866,6 +1873,7 @@ public AiPreferences getAiPreferences() {
EasyBind.listen(aiPreferences.mistralAiApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_MISTRAL_AI_API_BASE_URL, newValue));
EasyBind.listen(aiPreferences.geminiApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_GEMINI_API_BASE_URL, newValue));
EasyBind.listen(aiPreferences.huggingFaceApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_HUGGING_FACE_API_BASE_URL, newValue));
+ EasyBind.listen(aiPreferences.gpt4AllApiBaseUrlProperty(), (obs, oldValue, newValue) -> put(AI_GPT_4_ALL_API_BASE_URL, newValue));
EasyBind.listen(aiPreferences.embeddingModelProperty(), (obs, oldValue, newValue) -> put(AI_EMBEDDING_MODEL, newValue.name()));
EasyBind.listen(aiPreferences.instructionProperty(), (obs, oldValue, newValue) -> put(AI_SYSTEM_MESSAGE, newValue));
diff --git a/src/main/java/org/jabref/model/ai/AiProvider.java b/src/main/java/org/jabref/model/ai/AiProvider.java
index 00382e98d36..053406fdc5f 100644
--- a/src/main/java/org/jabref/model/ai/AiProvider.java
+++ b/src/main/java/org/jabref/model/ai/AiProvider.java
@@ -3,10 +3,11 @@
import java.io.Serializable;
public enum AiProvider implements Serializable {
- OPEN_AI("OpenAI", "https://openai.com/policies/privacy-policy/", "https://openai.com/policies/privacy-policy/"),
+ OPEN_AI("OpenAI", "https://api.openai.com/v1", "https://openai.com/policies/privacy-policy/"),
MISTRAL_AI("Mistral AI", "https://mistral.ai/terms/#privacy-policy", "https://mistral.ai/terms/#privacy-policy"),
GEMINI("Gemini", "https://huggingface.co/privacy", "https://ai.google.dev/gemini-api/terms"),
- HUGGING_FACE("Hugging Face", "https://huggingface.co/api", "https://huggingface.co/privacy");
+ HUGGING_FACE("Hugging Face", "https://huggingface.co/api", "https://huggingface.co/privacy"),
+ GPT4ALL("GPT4All", "http://localhost:4891/v1", "https://www.nomic.ai/gpt4all/legal/privacy-policy");
private final String label;
private final String apiUrl;