Skip to content

Commit

Permalink
#4614 - Display suggestions from Named Entity Linker immediately
Browse files Browse the repository at this point in the history
- Added ability to internally mark recommenders as synchronous. Such recommenders are run immedately after creating an annotation during the very same request such that suggestions appear immediately.
  • Loading branch information
reckart committed Mar 17, 2024
1 parent 529f467 commit 5d31c95
Show file tree
Hide file tree
Showing 10 changed files with 142 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.uima.cas.CAS;
Expand Down Expand Up @@ -139,9 +138,9 @@ private void predictSingle(String aCoveredText, int aBegin, int aEnd, CAS aCas)
Feature predictedFeature = getPredictedFeature(aCas);
Feature isPredictionFeature = getIsPredictionFeature(aCas);

for (KBHandle prediction : handles.stream().limit(recommender.getMaxRecommendations())
.collect(Collectors.toList())) {
AnnotationFS annotation = aCas.createAnnotation(predictedType, aBegin, aEnd);
for (var prediction : handles.stream().limit(recommender.getMaxRecommendations())
.toList()) {
var annotation = aCas.createAnnotation(predictedType, aBegin, aEnd);
annotation.setStringValue(predictedFeature, prediction.getIdentifier());
annotation.setBooleanValue(isPredictionFeature, true);
aCas.addFsToIndexes(annotation);
Expand All @@ -164,7 +163,7 @@ public TrainingCapability getTrainingCapability()
@Override
public EvaluationResult evaluate(List<CAS> aCasses, DataSplitter aDataSplitter)
{
EvaluationResult result = new EvaluationResult();
var result = new EvaluationResult();
result.setEvaluationSkipped(true);
result.setErrorMsg("NamedEntityLinker does not support evaluation.");
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ public String getId()
return ID;
}

@Override
public boolean isSynchronous()
{
return true;
}

@Override
public RecommendationEngine build(Recommender aRecommender)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,41 +150,41 @@ private Builder()

public Builder withModel(String aModel)
{
this.model = aModel;
model = aModel;
return this;
}

public Builder withPrompt(String aPrompt)
{
this.prompt = aPrompt;
prompt = aPrompt;
return this;
}

public Builder withFormat(OllamaGenerateResponseFormat aFormat)
{
this.format = aFormat;
format = aFormat;
return this;
}

public Builder withStream(boolean aStream)
{
this.stream = aStream;
stream = aStream;
return this;
}

public Builder withRaw(boolean aRaw)
{
this.raw = aRaw;
raw = aRaw;
return this;
}

public <T> Builder withOption(Option<T> aOption, T aValue)
{
if (aValue != null) {
this.options.put(aOption.getName(), aValue);
options.put(aOption.getName(), aValue);
}
else {
this.options.remove(aOption.getName());
options.remove(aOption.getName());
}
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ Optional<EvaluatedRecommender> getEvaluatedRecommender(User aSessionOwner,
*/
boolean switchPredictions(String aSessionOwner, Project aProject);

boolean forceSwitchPredictions(String aSessionOwner, Project aProject);

/**
* Returns the {@code RecommenderContext} for the given recommender if it exists.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import org.springframework.context.ApplicationEvent;

import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument;
import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState;
import de.tudarmstadt.ukp.inception.support.wicket.event.HybridApplicationUIEvent;

public class PredictionsSwitchedEvent
Expand All @@ -29,25 +28,20 @@ public class PredictionsSwitchedEvent
{
private static final long serialVersionUID = 3072280760236986642L;

private final AnnotatorState state;
private final String sessionOwner;
private final SourceDocument document;

public PredictionsSwitchedEvent(Object aSource, String aSessionOwner, AnnotatorState aState)
public PredictionsSwitchedEvent(Object aSource, String aSessionOwner, SourceDocument aDocument)
{
super(aSource);
state = aState;
sessionOwner = aSessionOwner;
}

public AnnotatorState getState()
{
return state;
document = aDocument;
}

@Override
public SourceDocument getDocument()
{
return state.getDocument();
return document;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public interface RecommendationEngineFactory<T>
*/
boolean isDeprecated();

default boolean isSynchronous()
{
return false;
}

default boolean isEvaluable()
{
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
public abstract class RecommendationEngineFactoryImplBase<T>
implements RecommendationEngineFactory<T>
{
private Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

@Override
public AbstractTraitsEditor createTraitsEditor(String aId, IModel<Recommender> aModel)
Expand Down Expand Up @@ -65,7 +65,7 @@ public T readTraits(Recommender aRecommender)
traits = fromJsonString((Class<T>) createTraits().getClass(), aRecommender.getTraits());
}
catch (IOException e) {
log.error("Error while reading traits", e);
LOG.error("Error while reading traits", e);
}

if (traits == null) {
Expand All @@ -83,7 +83,7 @@ public void writeTraits(Recommender aRecommender, T aTraits)
aRecommender.setTraits(json);
}
catch (IOException e) {
log.error("Error while writing traits", e);
LOG.error("Error while writing traits", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ public void renderRequested(AjaxRequestTarget aTarget, AnnotatorState aState)

// do not show predictions during curation or when viewing others' work
var sessionOwner = userService.getCurrentUsername();
if (!aState.getMode().equals(ANNOTATION)) {
if (aState.getMode() != ANNOTATION) {
return;
}

Expand All @@ -280,8 +280,8 @@ public void renderRequested(AjaxRequestTarget aTarget, AnnotatorState aState)

// Notify other UI components on the page about the prediction switch such that they can
// also update their state to remain in sync with the new predictions
applicationEventPublisher
.publishEvent(new PredictionsSwitchedEvent(this, sessionOwner, aState));
applicationEventPublisher.publishEvent(
new PredictionsSwitchedEvent(this, sessionOwner, aState.getDocument()));

aTarget.appendJavaScript("document.body.classList.remove('"
+ RecommenderActionBarPanel.STATE_PREDICTIONS_AVAILABLE + "')");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderFactoryRegistry;
import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport;
import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupportRegistry;
import de.tudarmstadt.ukp.inception.recommendation.api.event.PredictionsSwitchedEvent;
import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion;
import de.tudarmstadt.ukp.inception.recommendation.api.model.AutoAcceptMode;
import de.tudarmstadt.ukp.inception.recommendation.api.model.EvaluatedRecommender;
Expand Down Expand Up @@ -723,10 +724,13 @@ public void onAfterCasWritten(AfterCasWrittenEvent aEvent)
return;
}

if (!existsEnabledRecommender(aEvent.getDocument().getProject())) {
var recommenders = listEnabledRecommenders(aEvent.getDocument().getProject());
if (recommenders.isEmpty()) {
return;
}

runSynchronousRecommenders(aEvent, recommenders);

var committed = requestCycle.getMetaData(COMMITTED);
if (committed == null) {
committed = new HashSet<>();
Expand Down Expand Up @@ -768,6 +772,37 @@ public void onAfterCasWritten(AfterCasWrittenEvent aEvent)
}
}

private void runSynchronousRecommenders(AfterCasWrittenEvent aEvent,
List<Recommender> recommenders)
{
var sessionOwner = userRepository.getCurrentUser();
var anySyncRan = false;
for (var recommender : recommenders) {
var factory = getRecommenderFactory(recommender);
if (factory.map(RecommendationEngineFactory::isSynchronous).orElse(false)) {
var predictionTask = PredictionTask.builder() //
.withSessionOwner(sessionOwner) //
.withTrigger("Synchronous prediction") //
.withCurrentDocument(aEvent.getDocument().getDocument()) //
.withDataOwner(aEvent.getDocument().getUser()) //
.withRecommender(recommender) //
.build();
schedulingService.executeSync(predictionTask);
anySyncRan = true;
}
}
if (anySyncRan) {
var switched = forceSwitchPredictions(sessionOwner.getUsername(),
aEvent.getDocument().getProject());
if (switched) {
// Notify other UI components on the page about the prediction switch such that they
// can also update their state to remain in sync with the new predictions
applicationEventPublisher.publishEvent(new PredictionsSwitchedEvent(this,
sessionOwner.getUsername(), aEvent.getDocument().getDocument()));
}
}
}

@EventListener
public void onRecommenderUpdated(RecommenderUpdatedEvent aEvent)
{
Expand Down Expand Up @@ -1114,6 +1149,15 @@ public boolean switchPredictions(String aSessionOwner, Project aProject)
}
}

@Override
public boolean forceSwitchPredictions(String aSessionOwner, Project aProject)
{
var state = getState(aSessionOwner, aProject);
synchronized (state) {
return state.forceSwitchPredictions();
}
}

@Override
public Optional<RecommenderContext> getContext(String aSessionOwner, Recommender aRecommender)
{
Expand Down Expand Up @@ -1405,11 +1449,21 @@ public Predictions getIncomingPredictions()
}

public boolean switchPredictions()
{
return switchPredictions(false);
}

public boolean forceSwitchPredictions()
{
return switchPredictions(true);
}

private boolean switchPredictions(boolean aForce)
{
// If the predictions have already been switched, do not switch again
RequestCycle requestCycle = RequestCycle.get();
if (requestCycle != null) {
Boolean switched = requestCycle.getMetaData(PredictionSwitchPerformedKey.INSTANCE);
var requestCycle = RequestCycle.get();
if (!aForce && requestCycle != null) {
var switched = requestCycle.getMetaData(PredictionSwitchPerformedKey.INSTANCE);
if (switched != null && switched) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ public class PredictionTask
private final String dataOwner;
private final boolean isolated;
private final Recommender recommender;
private final boolean synchronousRecommenders;
private final boolean asynchronousRecommenders;

private Predictions predictions;

Expand All @@ -108,6 +110,8 @@ public PredictionTask(Builder<? extends Builder<?>> aBuilder)
predictionEnd = aBuilder.predictionEnd;
isolated = aBuilder.isolated;
recommender = aBuilder.recommender;
synchronousRecommenders = aBuilder.synchronousRecommenders;
asynchronousRecommenders = aBuilder.asynchronousRecommenders;
}

/**
Expand Down Expand Up @@ -405,6 +409,16 @@ private void applySingleRecomenderToDocument(LazyCas aOriginalCas, Recommender a
}
var factory = maybeFactory.get();

if (factory.isSynchronous() && !synchronousRecommenders) {
logSkippingSynchronous(aPredictions, aRecommender);
return;
}

if (!factory.isSynchronous() && !asynchronousRecommenders) {
logSkippingAsynchronous(aPredictions, aRecommender);
return;
}

// Check that configured layer and feature are accepted by this type of recommender
if (!factory.accepts(aRecommender.getLayer(), aRecommender.getFeature())) {
logInvalidRecommenderConfiguration(aPredictions, aRecommender);
Expand Down Expand Up @@ -820,6 +834,26 @@ private void logRecommenderContextNoReady(Predictions aPredictions, SourceDocume
aDocument.getProject());
}

private void logSkippingSynchronous(Predictions aPredictions, Recommender aRecommender)
{
aPredictions.log(LogMessage.info(aRecommender.getName(),
"Synchronous recommenders disabled in this run... skipping"));
LOG.info(
"[{}][{}]: Synchronous recommenders disabled in this run "
+ "- skipping recommender",
getSessionOwner().getUsername(), aRecommender.getName());
}

private void logSkippingAsynchronous(Predictions aPredictions, Recommender aRecommender)
{
aPredictions.log(LogMessage.info(aRecommender.getName(),
"Asynchronous recommenders disabled in this run... skipping"));
LOG.info(
"[{}][{}]: Asynchronous recommenders disabled in this run "
+ "- skipping recommender",
getSessionOwner().getUsername(), aRecommender.getName());
}

private void logInvalidRecommenderConfiguration(Predictions aPredictions,
Recommender aRecommender)
{
Expand Down Expand Up @@ -951,6 +985,8 @@ public static class Builder<T extends Builder<?>>
private int predictionBegin = -1;
private int predictionEnd = -1;
private boolean isolated = false;
private boolean asynchronousRecommenders = true;
private boolean synchronousRecommenders = true;

/**
* Generate predictions only for the specified recommender. If this is not set, then
Expand Down Expand Up @@ -1016,6 +1052,20 @@ public T withIsolated(boolean aIsolated)
return (T) this;
}

@SuppressWarnings("unchecked")
public T withAsynchronousRecommenders(boolean aFlag)
{
asynchronousRecommenders = aFlag;
return (T) this;
}

@SuppressWarnings("unchecked")
public T withSynchronousRecommenders(boolean aFlag)
{
synchronousRecommenders = aFlag;
return (T) this;
}

public PredictionTask build()
{
Validate.notNull(sessionOwner, "SelectionTask requires a user");
Expand Down

0 comments on commit 5d31c95

Please sign in to comment.