diff --git a/src/main/java/org/jabref/gui/actions/StandardActions.java b/src/main/java/org/jabref/gui/actions/StandardActions.java index 76d5aa0413a..df789a51733 100644 --- a/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -193,6 +193,8 @@ public enum StandardActions implements Action { GROUP_REMOVE_WITH_SUBGROUPS(Localization.lang("Also remove subgroups")), GROUP_CHAT(Localization.lang("Chat with group")), GROUP_EDIT(Localization.lang("Edit group")), + GROUP_GENERATE_SUMMARIES(Localization.lang("Generate summaries for entries in the group")), + GROUP_GENERATE_EMBEDDINGS(Localization.lang("Generate embeddings for linked files in the group")), GROUP_SUBGROUP_ADD(Localization.lang("Add subgroup")), GROUP_SUBGROUP_REMOVE(Localization.lang("Remove subgroups")), GROUP_SUBGROUP_SORT(Localization.lang("Sort subgroups A-Z")), diff --git a/src/main/java/org/jabref/gui/frame/JabRefFrame.java b/src/main/java/org/jabref/gui/frame/JabRefFrame.java index 573e8eb2a17..c40a946cffc 100644 --- a/src/main/java/org/jabref/gui/frame/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/frame/JabRefFrame.java @@ -153,10 +153,10 @@ public JabRefFrame(Stage mainStage, this.sidePane = new SidePane( this, this.preferences, - aiService, Injector.instantiateModelOrService(JournalAbbreviationRepository.class), taskExecutor, dialogService, + aiService, stateManager, fileUpdateMonitor, entryTypesManager, diff --git a/src/main/java/org/jabref/gui/groups/GroupTreeView.java b/src/main/java/org/jabref/gui/groups/GroupTreeView.java index 4b39d4536db..7b6efa2ecdd 100644 --- a/src/main/java/org/jabref/gui/groups/GroupTreeView.java +++ b/src/main/java/org/jabref/gui/groups/GroupTreeView.java @@ -555,6 +555,8 @@ private ContextMenu createContextMenuForGroup(GroupNodeViewModel group) { contextMenu.getItems().addAll( factory.createMenuItem(StandardActions.GROUP_EDIT, new ContextAction(StandardActions.GROUP_EDIT, group)), + factory.createMenuItem(StandardActions.GROUP_GENERATE_EMBEDDINGS, new ContextAction(StandardActions.GROUP_GENERATE_EMBEDDINGS, group)), + factory.createMenuItem(StandardActions.GROUP_GENERATE_SUMMARIES, new ContextAction(StandardActions.GROUP_GENERATE_SUMMARIES, group)), removeGroup, new SeparatorMenuItem(), factory.createMenuItem(StandardActions.GROUP_SUBGROUP_ADD, new ContextAction(StandardActions.GROUP_SUBGROUP_ADD, group)), @@ -668,6 +670,10 @@ public void execute() { viewModel.editGroup(group); groupTree.refresh(); } + case GROUP_GENERATE_EMBEDDINGS -> + viewModel.generateEmbeddings(group); + case GROUP_GENERATE_SUMMARIES -> + viewModel.generateSummaries(group); case GROUP_CHAT -> viewModel.chatWithGroup(group); case GROUP_SUBGROUP_ADD -> diff --git a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java index 5575fa6837f..48def6325c8 100644 --- a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java @@ -32,6 +32,7 @@ import org.jabref.logic.util.TaskExecutor; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; import org.jabref.model.groups.AbstractGroup; import org.jabref.model.groups.AutomaticKeywordGroup; import org.jabref.model.groups.AutomaticPersonsGroup; @@ -390,11 +391,7 @@ public void editGroup(GroupNodeViewModel oldGroup) { } public void chatWithGroup(GroupNodeViewModel group) { - // This should probably be done some other way. Please don't blame, it's just a thing to make it quick and fast. - if (currentDatabase.isEmpty()) { - dialogService.showErrorDialogAndWait(Localization.lang("Unable to chat with group"), Localization.lang("No library is selected.")); - return; - } + assert currentDatabase.isPresent(); StringProperty groupNameProperty = group.getGroupNode().getGroup().nameProperty(); @@ -434,6 +431,51 @@ private void openAiChat(StringProperty name, ObservableList chatHis } } + public void generateEmbeddings(GroupNodeViewModel groupNode) { + assert currentDatabase.isPresent(); + + AbstractGroup group = groupNode.getGroupNode().getGroup(); + + List linkedFiles = currentDatabase + .get() + .getDatabase() + .getEntries() + .stream() + .filter(group::isMatch) + .flatMap(entry -> entry.getFiles().stream()) + .toList(); + + aiService.getIngestionService().ingest( + group.nameProperty(), + linkedFiles, + currentDatabase.get() + ); + + dialogService.notify(Localization.lang("Ingestion started for group \"%0\".", group.getName())); + } + + public void generateSummaries(GroupNodeViewModel groupNode) { + assert currentDatabase.isPresent(); + + AbstractGroup group = groupNode.getGroupNode().getGroup(); + + List entries = currentDatabase + .get() + .getDatabase() + .getEntries() + .stream() + .filter(group::isMatch) + .toList(); + + aiService.getSummariesService().summarize( + group.nameProperty(), + entries, + currentDatabase.get() + ); + + dialogService.notify(Localization.lang("Summarization started for group \"%0\".", group.getName())); + } + public void removeSubgroups(GroupNodeViewModel group) { boolean confirmation = dialogService.showConfirmationDialogAndWait( Localization.lang("Remove subgroups"), diff --git a/src/main/java/org/jabref/gui/sidepane/SidePane.java b/src/main/java/org/jabref/gui/sidepane/SidePane.java index f4bace766a8..457a9f28c65 100644 --- a/src/main/java/org/jabref/gui/sidepane/SidePane.java +++ b/src/main/java/org/jabref/gui/sidepane/SidePane.java @@ -33,10 +33,10 @@ public class SidePane extends VBox { public SidePane(LibraryTabContainer tabContainer, GuiPreferences preferences, - AiService aiService, JournalAbbreviationRepository abbreviationRepository, TaskExecutor taskExecutor, DialogService dialogService, + AiService aiService, StateManager stateManager, FileUpdateMonitor fileUpdateMonitor, BibEntryTypesManager entryTypesManager, @@ -47,11 +47,11 @@ public SidePane(LibraryTabContainer tabContainer, this.viewModel = new SidePaneViewModel( tabContainer, preferences, - aiService, abbreviationRepository, stateManager, taskExecutor, dialogService, + aiService, fileUpdateMonitor, entryTypesManager, clipBoardManager, diff --git a/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java b/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java index bd6027ddea5..d8eee75d756 100644 --- a/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java +++ b/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java @@ -22,10 +22,10 @@ public class SidePaneContentFactory { private final LibraryTabContainer tabContainer; private final GuiPreferences preferences; - private final AiService aiService; private final JournalAbbreviationRepository abbreviationRepository; private final TaskExecutor taskExecutor; private final DialogService dialogService; + private final AiService aiService; private final StateManager stateManager; private final FileUpdateMonitor fileUpdateMonitor; private final BibEntryTypesManager entryTypesManager; @@ -34,10 +34,10 @@ public class SidePaneContentFactory { public SidePaneContentFactory(LibraryTabContainer tabContainer, GuiPreferences preferences, - AiService aiService, JournalAbbreviationRepository abbreviationRepository, TaskExecutor taskExecutor, DialogService dialogService, + AiService aiService, StateManager stateManager, FileUpdateMonitor fileUpdateMonitor, BibEntryTypesManager entryTypesManager, @@ -45,10 +45,10 @@ public SidePaneContentFactory(LibraryTabContainer tabContainer, UndoManager undoManager) { this.tabContainer = tabContainer; this.preferences = preferences; - this.aiService = aiService; this.abbreviationRepository = abbreviationRepository; this.taskExecutor = taskExecutor; this.dialogService = dialogService; + this.aiService = aiService; this.stateManager = stateManager; this.fileUpdateMonitor = fileUpdateMonitor; this.entryTypesManager = entryTypesManager; diff --git a/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java b/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java index 344ee5b1381..b4d13c70d9f 100644 --- a/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java +++ b/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java @@ -42,11 +42,11 @@ public class SidePaneViewModel extends AbstractViewModel { public SidePaneViewModel(LibraryTabContainer tabContainer, GuiPreferences preferences, - AiService aiService, JournalAbbreviationRepository abbreviationRepository, StateManager stateManager, TaskExecutor taskExecutor, DialogService dialogService, + AiService aiService, FileUpdateMonitor fileUpdateMonitor, BibEntryTypesManager entryTypesManager, ClipBoardManager clipBoardManager, @@ -57,10 +57,10 @@ public SidePaneViewModel(LibraryTabContainer tabContainer, this.sidePaneContentFactory = new SidePaneContentFactory( tabContainer, preferences, - aiService, abbreviationRepository, taskExecutor, dialogService, + aiService, stateManager, fileUpdateMonitor, entryTypesManager, diff --git a/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsForSeveralTask.java b/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsForSeveralTask.java index 0bb1626a035..6427a08de49 100644 --- a/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsForSeveralTask.java +++ b/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsForSeveralTask.java @@ -29,7 +29,7 @@ public class GenerateEmbeddingsForSeveralTask extends BackgroundTask { private static final Logger LOGGER = LoggerFactory.getLogger(GenerateEmbeddingsForSeveralTask.class); - private final StringProperty name; + private final StringProperty groupName; private final List> linkedFiles; private final FileEmbeddingsManager fileEmbeddingsManager; private final BibDatabaseContext bibDatabaseContext; @@ -42,7 +42,7 @@ public class GenerateEmbeddingsForSeveralTask extends BackgroundTask { private String currentFile = ""; public GenerateEmbeddingsForSeveralTask( - StringProperty name, + StringProperty groupName, List> linkedFiles, FileEmbeddingsManager fileEmbeddingsManager, BibDatabaseContext bibDatabaseContext, @@ -50,7 +50,7 @@ public GenerateEmbeddingsForSeveralTask( TaskExecutor taskExecutor, ReadOnlyBooleanProperty shutdownSignal ) { - this.name = name; + this.groupName = groupName; this.linkedFiles = linkedFiles; this.fileEmbeddingsManager = fileEmbeddingsManager; this.bibDatabaseContext = bibDatabaseContext; @@ -58,7 +58,7 @@ public GenerateEmbeddingsForSeveralTask( this.taskExecutor = taskExecutor; this.shutdownSignal = shutdownSignal; - configure(name); + configure(groupName); } private void configure(StringProperty name) { @@ -73,9 +73,10 @@ private void configure(StringProperty name) { @Override public Void call() throws Exception { - LOGGER.debug("Starting embeddings generation of several files for {}", name.get()); + LOGGER.debug("Starting embeddings generation of several files for {}", groupName.get()); List, String>> futures = new ArrayList<>(); + linkedFiles .stream() .map(processingInfo -> { @@ -88,6 +89,7 @@ public Void call() throws Exception { filePreferences, shutdownSignal ) + .showToUser(false) .onSuccess(v -> processingInfo.setState(ProcessingState.SUCCESS)) .onFailure(processingInfo::setException) .onFinished(() -> progressCounter.increaseWorkDone(1)) @@ -101,7 +103,7 @@ public Void call() throws Exception { pair.getKey().get(); } - LOGGER.debug("Finished embeddings generation task of several files for {}", name.get()); + LOGGER.debug("Finished embeddings generation task of several files for {}", groupName.get()); progressCounter.stop(); return null; } diff --git a/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsTask.java b/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsTask.java index 11bf406ca55..219217be8bc 100644 --- a/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsTask.java +++ b/src/main/java/org/jabref/logic/ai/ingestion/GenerateEmbeddingsTask.java @@ -48,10 +48,11 @@ public GenerateEmbeddingsTask(LinkedFile linkedFile, this.filePreferences = filePreferences; this.shutdownSignal = shutdownSignal; - configure(linkedFile); + configure(); } - private void configure(LinkedFile linkedFile) { + private void configure() { + showToUser(true); titleProperty().set(Localization.lang("Generating embeddings for file '%0'", linkedFile.getLink())); progressCounter.listenToAllProperties(this::updateProgress); diff --git a/src/main/java/org/jabref/logic/ai/ingestion/IngestionService.java b/src/main/java/org/jabref/logic/ai/ingestion/IngestionService.java index d38940002e6..fd5812cfe7e 100644 --- a/src/main/java/org/jabref/logic/ai/ingestion/IngestionService.java +++ b/src/main/java/org/jabref/logic/ai/ingestion/IngestionService.java @@ -124,20 +124,24 @@ public List> getProcessingInfo(List return linkedFiles.stream().map(this::getProcessingInfo).toList(); } - public List> ingest(StringProperty name, List linkedFiles, BibDatabaseContext bibDatabaseContext) { + public List> ingest(StringProperty groupName, List linkedFiles, BibDatabaseContext bibDatabaseContext) { List> result = getProcessingInfo(linkedFiles); if (listsUnderIngestion.contains(linkedFiles)) { return result; } + listsUnderIngestion.add(linkedFiles); + List> needToProcess = result.stream().filter(processingInfo -> processingInfo.getState() == ProcessingState.STOPPED).toList(); - startEmbeddingsGenerationTask(name, needToProcess, bibDatabaseContext); + startEmbeddingsGenerationTask(groupName, needToProcess, bibDatabaseContext); return result; } private void startEmbeddingsGenerationTask(LinkedFile linkedFile, BibDatabaseContext bibDatabaseContext, ProcessingInfo processingInfo) { + processingInfo.setState(ProcessingState.PROCESSING); + new GenerateEmbeddingsTask(linkedFile, fileEmbeddingsManager, bibDatabaseContext, filePreferences, shutdownSignal) .showToUser(true) .onSuccess(v -> processingInfo.setState(ProcessingState.SUCCESS)) @@ -145,8 +149,10 @@ private void startEmbeddingsGenerationTask(LinkedFile linkedFile, BibDatabaseCon .executeWith(taskExecutor); } - private void startEmbeddingsGenerationTask(StringProperty name, List> linkedFiles, BibDatabaseContext bibDatabaseContext) { - new GenerateEmbeddingsForSeveralTask(name, linkedFiles, fileEmbeddingsManager, bibDatabaseContext, filePreferences, taskExecutor, shutdownSignal) + private void startEmbeddingsGenerationTask(StringProperty groupName, List> linkedFiles, BibDatabaseContext bibDatabaseContext) { + linkedFiles.forEach(processingInfo -> processingInfo.setState(ProcessingState.PROCESSING)); + + new GenerateEmbeddingsForSeveralTask(groupName, linkedFiles, fileEmbeddingsManager, bibDatabaseContext, filePreferences, taskExecutor, shutdownSignal) .executeWith(taskExecutor); } diff --git a/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryForSeveralTask.java b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryForSeveralTask.java new file mode 100644 index 00000000000..4940fba8b21 --- /dev/null +++ b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryForSeveralTask.java @@ -0,0 +1,125 @@ +package org.jabref.logic.ai.summarization; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; + +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.StringProperty; +import javafx.util.Pair; + +import org.jabref.logic.FilePreferences; +import org.jabref.logic.ai.AiPreferences; +import org.jabref.logic.ai.processingstatus.ProcessingInfo; +import org.jabref.logic.ai.processingstatus.ProcessingState; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.BackgroundTask; +import org.jabref.logic.util.ProgressCounter; +import org.jabref.logic.util.TaskExecutor; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; + +import dev.langchain4j.model.chat.ChatLanguageModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This task generates summaries for several {@link BibEntry}ies (typically used for groups). + * It will check if summaries were already generated. + * And it also will store the summaries. + */ +public class GenerateSummaryForSeveralTask extends BackgroundTask { + private static final Logger LOGGER = LoggerFactory.getLogger(GenerateSummaryForSeveralTask.class); + + private final StringProperty groupName; + private final List> entries; + private final BibDatabaseContext bibDatabaseContext; + private final SummariesStorage summariesStorage; + private final ChatLanguageModel chatLanguageModel; + private final ReadOnlyBooleanProperty shutdownSignal; + private final AiPreferences aiPreferences; + private final FilePreferences filePreferences; + private final TaskExecutor taskExecutor; + + private final ProgressCounter progressCounter = new ProgressCounter(); + + private String currentFile = ""; + + public GenerateSummaryForSeveralTask( + StringProperty groupName, + List> entries, + BibDatabaseContext bibDatabaseContext, + SummariesStorage summariesStorage, + ChatLanguageModel chatLanguageModel, + ReadOnlyBooleanProperty shutdownSignal, + AiPreferences aiPreferences, + FilePreferences filePreferences, + TaskExecutor taskExecutor + ) { + this.groupName = groupName; + this.entries = entries; + this.bibDatabaseContext = bibDatabaseContext; + this.summariesStorage = summariesStorage; + this.chatLanguageModel = chatLanguageModel; + this.shutdownSignal = shutdownSignal; + this.aiPreferences = aiPreferences; + this.filePreferences = filePreferences; + this.taskExecutor = taskExecutor; + + configure(); + } + + private void configure() { + showToUser(true); + titleProperty().set(Localization.lang("Generating summaries for %0", groupName.get())); + groupName.addListener((o, oldValue, newValue) -> titleProperty().set(Localization.lang("Generating summaries for %0", newValue))); + + progressCounter.increaseWorkMax(entries.size()); + progressCounter.listenToAllProperties(this::updateProgress); + updateProgress(); + } + + @Override + public Void call() throws Exception { + LOGGER.debug("Starting summaries generation of several files for {}", groupName.get()); + + List, BibEntry>> futures = new ArrayList<>(); + + entries + .stream() + .map(processingInfo -> { + processingInfo.setState(ProcessingState.PROCESSING); + return new Pair<>( + new GenerateSummaryTask( + processingInfo.getObject(), + bibDatabaseContext, + summariesStorage, + chatLanguageModel, + shutdownSignal, + aiPreferences, + filePreferences + ) + .showToUser(false) + .onSuccess(processingInfo::setSuccess) + .onFailure(processingInfo::setException) + .onFinished(() -> progressCounter.increaseWorkDone(1)) + .executeWith(taskExecutor), + processingInfo.getObject()); + }) + .forEach(futures::add); + + for (Pair, BibEntry> pair : futures) { + currentFile = pair.getValue().getCitationKey().orElse(""); + pair.getKey().get(); + } + + LOGGER.debug("Finished embeddings generation task of several files for {}", groupName.get()); + progressCounter.stop(); + return null; + } + + private void updateProgress() { + updateProgress(progressCounter.getWorkDone(), progressCounter.getWorkMax()); + updateMessage(progressCounter.getMessage() + " - " + currentFile + ", ..."); + } +} diff --git a/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java index 208de09f490..f863c8f01e0 100644 --- a/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java +++ b/src/main/java/org/jabref/logic/ai/summarization/GenerateSummaryTask.java @@ -1,6 +1,7 @@ package org.jabref.logic.ai.summarization; import java.nio.file.Path; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -8,15 +9,17 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; import org.jabref.logic.FilePreferences; import org.jabref.logic.ai.AiPreferences; import org.jabref.logic.ai.ingestion.FileToDocument; +import org.jabref.logic.ai.util.CitationKeyCheck; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.BackgroundTask; import org.jabref.logic.util.ProgressCounter; import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; import dev.langchain4j.data.document.Document; @@ -31,12 +34,12 @@ /** * This task generates a new summary for an entry. - * It will not check if summary was already generated. - * And it also does not store the summary. + * It will check if summary was already generated. + * And it also will store the summary. *

* This task is created in the {@link SummariesService}, and stored then in a {@link SummariesStorage}. */ -public class GenerateSummaryTask extends BackgroundTask { +public class GenerateSummaryTask extends BackgroundTask

{ private static final Logger LOGGER = LoggerFactory.getLogger(GenerateSummaryTask.class); // Be careful when constructing prompt. @@ -69,57 +72,89 @@ public class GenerateSummaryTask extends BackgroundTask { private static final int CHAR_TOKEN_FACTOR = 4; // Means, every token is roughly 4 characters. private final BibDatabaseContext bibDatabaseContext; + private final BibEntry entry; private final String citationKey; - private final List linkedFiles; private final ChatLanguageModel chatLanguageModel; - private final BooleanProperty shutdownSignal; + private final SummariesStorage summariesStorage; + private final ReadOnlyBooleanProperty shutdownSignal; private final AiPreferences aiPreferences; private final FilePreferences filePreferences; private final ProgressCounter progressCounter = new ProgressCounter(); - public GenerateSummaryTask(BibDatabaseContext bibDatabaseContext, - String citationKey, - List linkedFiles, + public GenerateSummaryTask(BibEntry entry, + BibDatabaseContext bibDatabaseContext, + SummariesStorage summariesStorage, ChatLanguageModel chatLanguageModel, - BooleanProperty shutdownSignal, + ReadOnlyBooleanProperty shutdownSignal, AiPreferences aiPreferences, FilePreferences filePreferences ) { this.bibDatabaseContext = bibDatabaseContext; - this.citationKey = citationKey; - this.linkedFiles = linkedFiles; + this.entry = entry; + this.citationKey = entry.getCitationKey().orElse(""); this.chatLanguageModel = chatLanguageModel; + this.summariesStorage = summariesStorage; this.shutdownSignal = shutdownSignal; this.aiPreferences = aiPreferences; this.filePreferences = filePreferences; - configure(citationKey); + configure(); } - private void configure(String citationKey) { - titleProperty().set(Localization.lang("Waiting summary for %0...", citationKey)); + private void configure() { showToUser(true); + titleProperty().set(Localization.lang("Waiting summary for %0...", citationKey)); progressCounter.listenToAllProperties(this::updateProgress); } @Override - public String call() throws Exception { + public Summary call() throws Exception { LOGGER.debug("Starting summarization task for entry {}", citationKey); - String result = null; + Optional savedSummary = Optional.empty(); - try { - result = summarizeAll(); - } catch (InterruptedException e) { - LOGGER.debug("There was a summarization task for {}. It will be canceled, because user quits JabRef.", citationKey); + if (bibDatabaseContext.getDatabasePath().isEmpty()) { + LOGGER.info("No database path is present. Summary will not be stored in the next sessions"); + } else if (entry.getCitationKey().isEmpty()) { + LOGGER.info("No citation key is present. Summary will not be stored in the next sessions"); + } else { + savedSummary = summariesStorage.get(bibDatabaseContext.getDatabasePath().get(), entry.getCitationKey().get()); + } + + Summary summary; + + if (savedSummary.isPresent()) { + summary = savedSummary.get(); + } else { + try { + String result = summarizeAll(); + + summary = new Summary( + LocalDateTime.now(), + aiPreferences.getAiProvider(), + aiPreferences.getSelectedChatModel(), + result + ); + } catch (InterruptedException e) { + LOGGER.debug("There was a summarization task for {}. It will be canceled, because user quits JabRef.", citationKey); + return null; + } + } + + if (bibDatabaseContext.getDatabasePath().isEmpty()) { + LOGGER.info("No database path is present. Summary will not be stored in the next sessions"); + } else if (CitationKeyCheck.citationKeyIsPresentAndUnique(bibDatabaseContext, entry)) { + LOGGER.info("No valid citation key is present. Summary will not be stored in the next sessions"); + } else { + summariesStorage.set(bibDatabaseContext.getDatabasePath().get(), entry.getCitationKey().get(), summary); } - showToUser(false); LOGGER.debug("Finished summarization task for entry {}", citationKey); progressCounter.stop(); - return result; + + return summary; } private String summarizeAll() throws InterruptedException { @@ -129,7 +164,7 @@ private String summarizeAll() throws InterruptedException { // Stream API would look better here, but we need to catch InterruptedException. List linkedFilesSummary = new ArrayList<>(); - for (LinkedFile linkedFile : linkedFiles) { + for (LinkedFile linkedFile : entry.getFiles()) { Optional s = generateSummary(linkedFile); if (s.isPresent()) { String string = s.get(); diff --git a/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java b/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java index 2411539e596..afac5e823f5 100644 --- a/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java +++ b/src/main/java/org/jabref/logic/ai/summarization/SummariesService.java @@ -1,11 +1,12 @@ package org.jabref.logic.ai.summarization; -import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.Comparator; -import java.util.Optional; +import java.util.List; import java.util.TreeMap; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.StringProperty; import org.jabref.logic.FilePreferences; import org.jabref.logic.ai.AiPreferences; @@ -38,6 +39,8 @@ public class SummariesService { private final TreeMap> summariesStatusMap = new TreeMap<>(Comparator.comparing(BibEntry::getId)); + private final List> listsUnderSummarization = new ArrayList<>(); + private final AiPreferences aiPreferences; private final SummariesStorage summariesStorage; private final ChatLanguageModel chatLanguageModel; @@ -96,27 +99,52 @@ public void listen(FieldChangedEvent e) { * on the same {@link BibEntry}, the method will return the same {@link ProcessingInfo}. */ public ProcessingInfo summarize(BibEntry bibEntry, BibDatabaseContext bibDatabaseContext) { - return summariesStatusMap.computeIfAbsent(bibEntry, file -> { - ProcessingInfo processingInfo = new ProcessingInfo<>(bibEntry, ProcessingState.PROCESSING); - generateSummary(bibEntry, bibDatabaseContext, processingInfo); - return processingInfo; - }); + ProcessingInfo processingInfo = getProcessingInfo(bibEntry); + + if (processingInfo.getState() == ProcessingState.STOPPED) { + startSummarizationTask(bibEntry, bibDatabaseContext, processingInfo); + } + + return processingInfo; } - private void generateSummary(BibEntry bibEntry, BibDatabaseContext bibDatabaseContext, ProcessingInfo processingInfo) { - if (bibDatabaseContext.getDatabasePath().isEmpty()) { - runGenerateSummaryTask(processingInfo, bibEntry, bibDatabaseContext); - } else if (bibEntry.getCitationKey().isEmpty() || CitationKeyCheck.citationKeyIsPresentAndUnique(bibDatabaseContext, bibEntry)) { - runGenerateSummaryTask(processingInfo, bibEntry, bibDatabaseContext); - } else { - Optional summary = summariesStorage.get(bibDatabaseContext.getDatabasePath().get(), bibEntry.getCitationKey().get()); + public ProcessingInfo getProcessingInfo(BibEntry entry) { + return summariesStatusMap.computeIfAbsent(entry, file -> new ProcessingInfo<>(entry, ProcessingState.STOPPED)); + } - if (summary.isEmpty()) { - runGenerateSummaryTask(processingInfo, bibEntry, bibDatabaseContext); - } else { - processingInfo.setSuccess(summary.get()); - } + public List> getProcessingInfo(List entries) { + return entries.stream().map(this::getProcessingInfo).toList(); + } + + public List> summarize(StringProperty groupName, List entries, BibDatabaseContext bibDatabaseContext) { + List> result = getProcessingInfo(entries); + + if (listsUnderSummarization.contains(entries)) { + return result; } + + listsUnderSummarization.add(entries); + + List> needToProcess = result.stream().filter(processingInfo -> processingInfo.getState() == ProcessingState.STOPPED).toList(); + startSummarizationTask(groupName, needToProcess, bibDatabaseContext); + + return result; + } + + private void startSummarizationTask(BibEntry entry, BibDatabaseContext bibDatabaseContext, ProcessingInfo processingInfo) { + processingInfo.setState(ProcessingState.PROCESSING); + + new GenerateSummaryTask(entry, bibDatabaseContext, summariesStorage, chatLanguageModel, shutdownSignal, aiPreferences, filePreferences) + .onSuccess(processingInfo::setSuccess) + .onFailure(processingInfo::setException) + .executeWith(taskExecutor); + } + + private void startSummarizationTask(StringProperty groupName, List> entries, BibDatabaseContext bibDatabaseContext) { + entries.forEach(processingInfo -> processingInfo.setState(ProcessingState.PROCESSING)); + + new GenerateSummaryForSeveralTask(groupName, entries, bibDatabaseContext, summariesStorage, chatLanguageModel, shutdownSignal, aiPreferences, filePreferences, taskExecutor) + .executeWith(taskExecutor); } /** @@ -134,37 +162,6 @@ public void regenerateSummary(BibEntry bibEntry, BibDatabaseContext bibDatabaseC summariesStorage.clear(bibDatabaseContext.getDatabasePath().get(), bibEntry.getCitationKey().get()); } - generateSummary(bibEntry, bibDatabaseContext, processingInfo); - } - - private void runGenerateSummaryTask(ProcessingInfo processingInfo, BibEntry bibEntry, BibDatabaseContext bibDatabaseContext) { - new GenerateSummaryTask( - bibDatabaseContext, - bibEntry.getCitationKey().orElse(""), - bibEntry.getFiles(), - chatLanguageModel, - shutdownSignal, - aiPreferences, - filePreferences) - .onSuccess(summary -> { - Summary Summary = new Summary( - LocalDateTime.now(), - aiPreferences.getAiProvider(), - aiPreferences.getSelectedChatModel(), - summary - ); - - processingInfo.setSuccess(Summary); - - if (bibDatabaseContext.getDatabasePath().isEmpty()) { - LOGGER.info("No database path is present. Summary will not be stored in the next sessions"); - } else if (CitationKeyCheck.citationKeyIsPresentAndUnique(bibDatabaseContext, bibEntry)) { - LOGGER.info("No valid citation key is present. Summary will not be stored in the next sessions"); - } else { - summariesStorage.set(bibDatabaseContext.getDatabasePath().get(), bibEntry.getCitationKey().get(), Summary); - } - }) - .onFailure(processingInfo::setException) - .executeWith(taskExecutor); + startSummarizationTask(bibEntry, bibDatabaseContext, processingInfo); } } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 589d990d104..0d980636e73 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2624,8 +2624,6 @@ Only\ PDF\ files\ can\ be\ used\ for\ chatting=Only PDF files can be used for ch The\ chat\ history\ will\ not\ be\ stored\ in\ next\ sessions=The chat history will not be stored in next sessions Unable\ to\ generate\ embeddings\ for\ file\ '%0',\ because\ JabRef\ was\ unable\ to\ extract\ text\ from\ the\ file=Unable to generate embeddings for file '%0', because JabRef was unable to extract text from the file Chat\ with\ group=Chat with group -No\ library\ is\ selected.=No library is selected. -Unable\ to\ chat\ with\ group=Unable to chat with group Waiting\ for\ AI\ reply...=Waiting for AI reply... An\ error\ occurred\ while\ opening\ chat\ history\ storage.\ Chat\ history\ of\ entries\ and\ groups\ will\ not\ be\ stored\ in\ the\ next\ session.=An error occurred while opening chat history storage. Chat history of entries and groups will not be stored in the next session. An\ error\ occurred\ while\ opening\ summary\ storage.\ Summaries\ of\ entries\ will\ not\ be\ stored\ in\ the\ next\ session.=An error occurred while opening summary storage. Summaries of entries will not be stored in the next session. @@ -2645,6 +2643,11 @@ If\ you\ have\ chosen\ %0\ as\ an\ AI\ provider,\ the\ privacy\ policy\ of\ %0\ Automatically\ generate\ embeddings\ for\ new\ entries=Automatically generate embeddings for new entries Automatically\ generate\ summaries\ for\ new\ entries=Automatically generate summaries for new entries Connection=Connection +Generate\ embeddings\ for\ linked\ files\ in\ the\ group=Generate embeddings for linked files in the group +Generate\ summaries\ for\ entries\ in\ the\ group=Generate summaries for entries in the group +Generating\ summaries\ for\ %0=Generating summaries for %0 +Ingestion\ started\ for\ group\ "%0".=Ingestion started for group "%0". +Summarization\ started\ for\ group\ "%0".=Summarization started for group "%0". Link=Link Source\ URL=Source URL diff --git a/src/test/java/org/jabref/gui/sidepane/SidePaneViewModelTest.java b/src/test/java/org/jabref/gui/sidepane/SidePaneViewModelTest.java index 9a5eeeac518..f61eea6dc51 100644 --- a/src/test/java/org/jabref/gui/sidepane/SidePaneViewModelTest.java +++ b/src/test/java/org/jabref/gui/sidepane/SidePaneViewModelTest.java @@ -38,11 +38,11 @@ class SidePaneViewModelTest { LibraryTabContainer tabContainer = mock(LibraryTabContainer.class); GuiPreferences preferences = mock(GuiPreferences.class); - AiService aiService = mock(AiService.class); JournalAbbreviationRepository abbreviationRepository = mock(JournalAbbreviationRepository.class); StateManager stateManager = mock(StateManager.class); TaskExecutor taskExecutor = mock(TaskExecutor.class); DialogService dialogService = mock(DialogService.class); + AiService aiService = mock(AiService.class); FileUpdateMonitor fileUpdateMonitor = mock(FileUpdateMonitor.class); BibEntryTypesManager entryTypesManager = mock(BibEntryTypesManager.class); ClipBoardManager clipBoardManager = mock(ClipBoardManager.class); @@ -67,11 +67,11 @@ void setUp() { sidePaneViewModel = new SidePaneViewModel( tabContainer, preferences, - aiService, abbreviationRepository, stateManager, taskExecutor, dialogService, + aiService, fileUpdateMonitor, entryTypesManager, clipBoardManager,