Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AI - MVP - v1 #1

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d9d58f6
First AI steps
InAnYan May 8, 2024
7c358c9
Some changes to AI code
InAnYan May 8, 2024
0a05213
Add AI preferences
InAnYan May 8, 2024
812820a
First part of fix
koppor May 8, 2024
7f92e0d
Fix: second part
koppor May 8, 2024
cee2278
More fixes
koppor May 8, 2024
37d0534
Remove unneded Apache PDFBox
koppor May 8, 2024
d9b6c3e
Added AiChatTab to EntryEditor tabs
InAnYan May 8, 2024
c0b56f3
Merge remote-tracking branch 'origin/ai-1' into ai-1
InAnYan May 8, 2024
19d8aa3
Fix dependency
koppor May 8, 2024
2dcaa3a
Add require on kotlin.stdlib
koppor May 8, 2024
f389bad
Some changes
InAnYan May 8, 2024
971fdce
AI preferences tab
InAnYan May 10, 2024
ddacd47
AI is split to classes. Preferences tab. UI changes
InAnYan May 11, 2024
1da98b5
Changed according to code review
InAnYan May 15, 2024
9ffeaa4
Deleted check for OpenAI token format
InAnYan May 15, 2024
3bf8ac1
Tried to solve the bug with token
InAnYan May 15, 2024
14b7c72
Tried to solve the bug with token
InAnYan May 15, 2024
784434f
Split preferences: "Show AI chat tab" / "Chat with PDFs"
InAnYan May 17, 2024
7fac426
Fix typos
InAnYan May 17, 2024
8a12515
Improve UI
InAnYan May 17, 2024
221f961
Save chat history in memory. Split UI to classes
InAnYan May 18, 2024
185de4e
Manage chat history
InAnYan May 19, 2024
267efc9
Clean saving chat history
InAnYan May 25, 2024
62ea22e
Fix module-info.java
InAnYan May 25, 2024
4ddf173
Fix SaveDatabaseAction
InAnYan May 25, 2024
f9bd0d0
Fix chat UI
InAnYan May 25, 2024
43845ae
Make custom components for UI
InAnYan May 25, 2024
c9cf1a0
Improve AI service
InAnYan May 27, 2024
6cec13a
Fix bugs
InAnYan May 27, 2024
066747c
Fix logger messages (changes provided by @koppor)
InAnYan May 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 47 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ version = project.findProperty('projVersion') ?: '100.0.0'
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21

// Workaround needed for Eclipse, probably because of https://github.com/gradle/gradle/issues/16922
// Should be removed as soon as Gradle 7.0.1 is released ( https://github.com/gradle/gradle/issues/16922#issuecomment-828217060 )
modularity.inferModulePath.set(false)
Expand Down Expand Up @@ -71,6 +72,11 @@ application {

// Note that the arguments are cleared for the "run" task to avoid messages like "WARNING: Unknown module: org.jabref.merged.module specified to --add-exports"

// '--add-exports=langchain4j/dev.langchain4j.model.chat=org.jabref',
// '--add-exports=langchain4j/dev.langchain4j.model.chat=org.jabref.merged.module',
// '--add-exports=langchain4j/dev.langchain4j.model.openai=org.jabref',
// '--add-exports=langchain4j/dev.langchain4j.model.openai=org.jabref.merged.module',

// Fix for https://github.com/JabRef/jabref/issues/11188
'--add-exports=javafx.base/com.sun.javafx.event=org.jabref.merged.module',
'--add-exports=javafx.controls/com.sun.javafx.scene.control=org.jabref.merged.module',
Expand All @@ -94,6 +100,13 @@ application {
// See also https://github.com/java9-modularity/gradle-modules-plugin/issues/165
modularity.disableEffectiveArgumentsAdjustment()

// Required as workaround for https://github.com/langchain4j/langchain4j/issues/1066
// modularity.patchModule("langchain4j", "langchain4j-0.28.0.jar")
modularity.patchModule("langchain4j", "langchain4j-core-0.28.0.jar")
modularity.patchModule("langchain4j", "langchain4j-embeddings-0.28.0.jar")
modularity.patchModule("langchain4j", "langchain4j-embeddings-all-minilm-l6-v2-0.28.0.jar")
modularity.patchModule("langchain4j", "langchain4j-open-ai-0.28.0.jar")

sourceSets {
main {
java {
Expand Down Expand Up @@ -219,14 +232,19 @@ dependencies {
implementation 'org.fxmisc.flowless:flowless:0.7.2'
implementation 'org.fxmisc.richtext:richtextfx:0.11.2'
implementation (group: 'com.dlsc.gemsfx', name: 'gemsfx', version: '2.12.0') {
exclude module: 'javax.inject' // Split package, use only jakarta.inject
exclude module: 'commons-lang3'
exclude module: 'javax.inject' // Split package, use only jakarta.inject
exclude module: 'kotlin-stdlib-jdk8'
exclude group: 'com.squareup.retrofit2'
exclude group: 'org.openjfx'
exclude group: 'org.apache.logging.log4j'
exclude group: 'tech.units'
}
// Required by gemsfx
implementation 'tech.units:indriya:2.2'
implementation ('com.squareup.retrofit2:retrofit:2.11.0') {
exclude group: 'com.squareup.okhttp3'
}

implementation 'org.controlsfx:controlsfx:11.2.1'

Expand Down Expand Up @@ -296,6 +314,19 @@ dependencies {
// YAML formatting
implementation 'org.yaml:snakeyaml:2.2'

// AI
implementation 'dev.langchain4j:langchain4j-embeddings-all-minilm-l6-v2:0.28.0'
implementation('dev.langchain4j:langchain4j-open-ai:0.28.0') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
}
// openai depends on okhttp, which needs kotlin - see https://github.com/square/okhttp/issues/5299 for details
// GemxFX also (transitively) depends on kotlin
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.24'

implementation ('com.squareup.okhttp3:okhttp:4.12.0') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
}

implementation 'commons-io:commons-io:2.16.1'

testImplementation 'io.github.classgraph:classgraph:4.8.172'
Expand Down Expand Up @@ -436,7 +467,18 @@ compileJava {
// TODO: Remove access to internal api
addExports = [
'javafx.controls/com.sun.javafx.scene.control' : 'org.jabref',
'org.controlsfx.controls/impl.org.controlsfx.skin' : 'org.jabref'
'org.controlsfx.controls/impl.org.controlsfx.skin' : 'org.jabref',

'langchain4j/dev.langchain4j.data.document' : 'org.jabref',
'langchain4j/dev.langchain4j.data.segment' : 'org.jabref',
'langchain4j/dev.langchain4j.model.chat' : 'org.jabref',
'langchain4j/dev.langchain4j.model.embedding' : 'org.jabref',
'langchain4j/dev.langchain4j.model.openai' : 'org.jabref',
'langchain4j/dev.langchain4j.rag.content.retriever' : 'org.jabref',
'langchain4j/dev.langchain4j.store.embedding' : 'org.jabref',
'langchain4j/dev.langchain4j.memory' : 'org.jabref',
'langchain4j/dev.langchain4j.store.memory.chat' : 'org.jabref',
'langchain4j/dev.langchain4j.data.message' : 'org.jabref',
]
}
}
Expand All @@ -455,6 +497,9 @@ run {
'javafx.base/com.sun.javafx.event' : 'org.jabref.merged.module',
'javafx.controls/com.sun.javafx.scene.control' : 'org.jabref',

'langchain4j/dev.langchain4j.model.chat' : 'org.jabref',
'langchain4j/dev.langchain4j.model.chat' : 'org.jabref.merged.module',

// We need to restate the ControlsFX exports, because we get following error otherwise:
// java.lang.IllegalAccessError:
// class org.controlsfx.control.textfield.AutoCompletionBinding (in module org.controlsfx.controls)
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@

requires org.jooq.jool;

// AI
requires langchain4j;
requires kotlin.stdlib;

// fulltext search
requires org.apache.lucene.core;
// In case the version is updated, please also adapt SearchFieldConstants#VERSION to the newly used version
Expand Down
238 changes: 238 additions & 0 deletions src/main/java/org/jabref/gui/entryeditor/AiChatTab.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package org.jabref.gui.entryeditor;

import java.util.List;

import javafx.geometry.Insets;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;

import org.jabref.gui.DialogService;
import org.jabref.logic.ai.AiChat;
import org.jabref.logic.ai.AiChatData;
import org.jabref.logic.ai.AiConnection;
import org.jabref.logic.ai.AiIngestor;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.LinkedFile;
import org.jabref.preferences.AiPreferences;
import org.jabref.preferences.FilePreferences;
import org.jabref.preferences.PreferencesService;

import com.tobiasdiez.easybind.EasyBind;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;

public class AiChatTab extends EntryEditorTab {
public static final String NAME = "AI chat";

private final DialogService dialogService;

private final FilePreferences filePreferences;
private final AiPreferences aiPreferences;

private final BibDatabaseContext bibDatabaseContext;

private AiConnection aiConnection = null;
private AiChat aiChat = null;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: Either always empty lines between variable declarations or never. Sometimes, I catch myself to mix too. However, then mostly there are JavaDoc comments the separted variables.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This applies for every-every field?
What if I have these fields in a class:

  • Logger
  • Data fields
  • Constants

I can't group them with new lines? And what will be the right order for this fields?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkstyle will tell about the order. Grouping is OK, add comments then. However, this could make the code more unreadable. Just keep one empty line between each "thing".

If reasonable, you could create a nested record class for more clearer grouping. However, in your case, this doesn't seem to be necessary.

private VBox chatVBox = null;

public AiChatTab(DialogService dialogService, PreferencesService preferencesService, BibDatabaseContext bibDatabaseContext) {
this.dialogService = dialogService;

this.filePreferences = preferencesService.getFilePreferences();
this.aiPreferences = preferencesService.getAiPreferences();

this.bibDatabaseContext = bibDatabaseContext;

setText(Localization.lang(NAME));
setTooltip(new Tooltip(Localization.lang("AI chat with full-text article")));

setUpAiConnection();
}

// Set up the AI connection if AI is used.
// Also listen for AI preferences changes and update the classes appropriately.
private void setUpAiConnection() {
if (aiPreferences.isUseAi()) {
aiConnection = new AiConnection(aiPreferences.getOpenAiToken());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something is wrong here.

  1. Start with unmodified JabRef preferences (use reset preferences)
  2. Open entry editor
  3. Go to preferences
  4. Insert an OpenAPI key and select "save"
  5. See an exception

grafik

java.lang.IllegalArgumentException: openAiApiKey cannot be null or empty. API keys can be generated here: https://platform.openai.com/account/api-keys
at [email protected]/dev.ai4j.openai4j.OpenAiClient$Builder.openAiApiKey(OpenAiClient.java:119)
at [email protected]/dev.langchain4j.model.openai.OpenAiChatModel.(OpenAiChatModel.java:80)
at [email protected]/dev.langchain4j.model.openai.OpenAiChatModel$OpenAiChatModelBuilder.build(OpenAiChatModel.java:50)
at [email protected]/org.jabref.logic.ai.AiConnection.(AiConnection.java:24)
at [email protected]/org.jabref.gui.entryeditor.AiChatTab.lambda$setUpAiConnection$0(AiChatTab.java:75)
at [email protected]/com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:372)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, very bad. I'll check that out.

The verification in AI preferences works like this:

  1. If you switched off "Use AI", then the user can exit this tab freely.
  2. But if "Use AI" is turned on, then the user have to provide an OpenAI key in proper format (O've found a regex for that 😁).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, I cannot reproduce the issue.

What is the right way to reset the JabRef settings? I've seen a button "Reset preferences" in Preferences window. Is that's it?

}

EasyBind.listen(aiPreferences.useAiProperty(), (obs, oldValue, newValue) -> {
if (newValue) {
aiConnection = new AiConnection(aiPreferences.getOpenAiToken());
rebuildAiChat();
} else {
aiConnection = null;
// QUESTION: If user chose AI but then unchooses, what should we do with the AI chat?
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case a user can "accidentally" disable the chat, we need to think. However, I think, it is an "informed" decision. Thus, we can just drop the chat. --> Just remove the comment line.

If we don't want to drop it, we should try to keep it: I don't know about the relation between aiConnection and aiChat.

aiChat = null;
}
});

EasyBind.listen(aiPreferences.openAiTokenProperty(), (obs, oldValue, newValue) -> {
if (aiConnection != null) {
aiConnection = new AiConnection(newValue);
rebuildAiChat();
}
});
}

private void rebuildAiChat() {
if (aiChat != null) {
AiChatData data = aiChat.getData();
aiChat = new AiChat(data, aiConnection);
}
}

@Override
public boolean shouldShow(BibEntry entry) {
return aiPreferences.isUseAi();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next iteration: Do it similar to the "recommendation" tab. The tab is shown as default, but asks for an OK

grafik


The checkmark "modern AI technologies" is too general. Think of the GDPR which asks software vendors to be very specific what happens with their data.

Thus, move from the following tab the setting to the "Entry editor" tab, next to the "recommendations". "Show AI Tab", and ask for the OpenAI token.

grafik

In case there will be more preferences, we first add them there. If they are too much, we might move the detail AI prrefernces to the separate tab "AI".

}

@Override
protected void bindToEntry(BibEntry entry) {
Node node;

if (entry.getFiles().isEmpty()) {
node = stateNoFiles();
} else if (!entry.getFiles().stream().allMatch(file -> "PDF".equals(file.getFileType()))) {
/*
QUESTION: What is the type of file.getFileType()????
I thought it is the part after the dot, but it turns out not.
I got the "PDF" string by looking at tests.
*/
node = stateWrongFilesFormat();
} else {
configureAiChat(entry);
node = stateAiChat();
restoreMessages(aiChat.getData().getChatMemoryStore().getMessages(aiChat.getChatId()));
}

setContent(node);
}

private Node stateNoFiles() {
return new Label(Localization.lang("No files attached"));
}

private Node stateWrongFilesFormat() {
return new Label(Localization.lang("Only PDF files are supported"));
}

private Node stateAiChat() {
// Don't bully me for this style.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, but autoformat will kill the indentation.

I am "working" to get palantir ready for this. That will reformat the whole source code. See JabRef#672 for details.

Just restructure in methods. Maybe, this is a point, where you need fxml? 😅

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really need advice on this topic.

Main challenges:

  • Some content is static, some is dynamic.
  • There are different UI states. Like, if not files are attached, then we don't need to load an FXML file. Or we will load a different FXML file.


VBox aiChatBox = new VBox(10);
aiChatBox.setPadding(new Insets(10));

ScrollPane chatScrollPane = new ScrollPane();
chatScrollPane.setStyle("-fx-border-color: black;");
chatScrollPane.setPadding(new Insets(10, 10, 0, 10));
VBox.setVgrow(chatScrollPane, Priority.ALWAYS);

chatVBox = new VBox(10);

// Chat messages will be children of chatVBox.

chatScrollPane.setContent(chatVBox);

aiChatBox.getChildren().add(chatScrollPane);

HBox userPromptHBox = new HBox(10);
userPromptHBox.setAlignment(Pos.CENTER);

TextField userPromptTextField = new TextField();
HBox.setHgrow(userPromptTextField, Priority.ALWAYS);

userPromptHBox.getChildren().add(userPromptTextField);

Button userPromptSubmitButton = new Button(Localization.lang("Submit"));
userPromptSubmitButton.setOnAction(e -> {
String userPrompt = userPromptTextField.getText();
userPromptTextField.setText("");

addMessage(true, userPrompt);

String aiMessage = aiChat.execute(userPrompt);

addMessage(false, aiMessage);
});

userPromptHBox.getChildren().add(userPromptSubmitButton);

aiChatBox.getChildren().add(userPromptHBox);

return aiChatBox;
}

private void addMessage(boolean isUser, String text) {
Node messageNode = generateMessage(isUser, text);
chatVBox.getChildren().add(messageNode);
}

private static final String USER_MESSAGE_COLOR = "#7ee3fb";
private static final String AI_MESSAGE_COLOR = "#bac8cb";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move that to the top of the class (so that all constants are grouped together).

For a PR to JabRef, that needs to be stylable via base.css, so that it can be themed.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where can I see an example of how other UI classes refer to base.css file, so I can learn from that and implement it here?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, haven't found it myself. But I've seen something in base.css. I'll show it in the next commit


private static Node generateMessage(boolean isUser, String text) {
Pane pane = new Pane();

if (isUser) {
pane.setNodeOrientation(NodeOrientation.RIGHT_TO_LEFT);
}

VBox paneVBox = new VBox(10);

paneVBox.setStyle("-fx-background-color: " + (isUser ? USER_MESSAGE_COLOR : AI_MESSAGE_COLOR) + ";");
paneVBox.setPadding(new Insets(10));

Label authorLabel = new Label(Localization.lang(isUser ? "User" : "AI"));
authorLabel.setStyle("-fx-font-weight: bold");
paneVBox.getChildren().add(authorLabel);

Label messageLabel = new Label(text);
paneVBox.getChildren().add(messageLabel);

pane.getChildren().add(paneVBox);

return pane;
}

private void configureAiChat(BibEntry entry) {
aiChat = new AiChat(aiConnection);

AiIngestor ingestor = new AiIngestor(aiChat.getData().getEmbeddingStore(), aiConnection.getEmbeddingModel());

for (LinkedFile linkedFile : entry.getFiles()) {
try {
ingestor.ingestLinkedFile(linkedFile, bibDatabaseContext, filePreferences);
} catch (Exception e) {
dialogService.showErrorDialogAndWait(Localization.lang("Error while loading file"),
Localization.lang("An error occurred while loading a file into the AI") + ":\n"
+ e.getMessage() + "\n"
+ Localization.lang("This file will be skipped") + ".");
}
}
}

private void restoreMessages(List<ChatMessage> messages) {
for (ChatMessage message : messages) {
if (message instanceof UserMessage userMessage) {
addMessage(true, userMessage.singleText());
} else if (message instanceof AiMessage aiMessage) {
addMessage(false, aiMessage.text());
}
}
}
}
1 change: 1 addition & 0 deletions src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ private List<EntryEditorTab> createTabs() {
entryEditorTabs.add(sourceTab);
entryEditorTabs.add(new LatexCitationsTab(databaseContext, preferencesService, dialogService, directoryMonitorManager));
entryEditorTabs.add(new FulltextSearchResultsTab(stateManager, preferencesService, dialogService, taskExecutor));
entryEditorTabs.add(new AiChatTab(dialogService, preferencesService, databaseContext));

return entryEditorTabs;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.jabref.gui.AbstractViewModel;
import org.jabref.gui.DialogService;
import org.jabref.gui.Globals;
import org.jabref.gui.preferences.ai.AiTab;
import org.jabref.gui.preferences.autocompletion.AutoCompletionTab;
import org.jabref.gui.preferences.citationkeypattern.CitationKeyPatternTab;
import org.jabref.gui.preferences.customentrytypes.CustomEntryTypesTab;
Expand Down Expand Up @@ -81,7 +82,8 @@ public PreferencesDialogViewModel(DialogService dialogService, PreferencesServic
new XmpPrivacyTab(),
new CustomImporterTab(),
new CustomExporterTab(),
new NetworkTab()
new NetworkTab(),
new AiTab()
);
}

Expand Down
16 changes: 16 additions & 0 deletions src/main/java/org/jabref/gui/preferences/ai/AiTab.fxml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<fx:root spacing="10.0" type="VBox" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.jabref.gui.preferences.ai.AiTab">
<children>
<CheckBox fx:id="useAi" mnemonicParsing="false" text="%Use modern AI technologies in JabRef" />
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label alignment="BASELINE_CENTER" text="%OpenAI API token" />
<TextField fx:id="openAiToken" HBox.hgrow="ALWAYS" />
</children>
</HBox>
</children>
</fx:root>
Loading