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;