Skip to content

Commit

Permalink
#2696 - Document-level recommendations
Browse files Browse the repository at this point in the history
- Start factoring layer-specifc recommender actions out of the recommender service
- Move events into the recommender API where they are more accessible
  • Loading branch information
reckart committed Dec 20, 2023
1 parent 9b1aeb8 commit 91180d3
Show file tree
Hide file tree
Showing 21 changed files with 714 additions and 205 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,6 @@ void rejectSpanSuggestion(String aSessionOwner, User aDataOwner, AnnotationLayer
SpanSuggestion aSuggestion);

void skipSpanSuggestion(String aSessionOwner, User aDataOwner, AnnotationLayer aLayer,
SpanSuggestion aSuggestion);
SpanSuggestion aSuggestion)
throws AnnotationException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ public void rejectSpanSuggestion(String aSessionOwner, User aDataOwner, Annotati
@Transactional
public void skipSpanSuggestion(String aSessionOwner, User aDataOwner, AnnotationLayer aLayer,
SpanSuggestion aSuggestion)
throws AnnotationException
{
var document = documentService.getSourceDocument(aLayer.getProject(),
aSuggestion.getDocumentName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@
import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler;
import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService;
import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService;
import de.tudarmstadt.ukp.inception.recommendation.api.event.AjaxRecommendationAcceptedEvent;
import de.tudarmstadt.ukp.inception.recommendation.api.event.AjaxRecommendationRejectedEvent;
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.LearningRecord;
import de.tudarmstadt.ukp.inception.recommendation.api.model.Offset;
Expand All @@ -102,9 +105,6 @@
import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup;
import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup;
import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup.Delta;
import de.tudarmstadt.ukp.inception.recommendation.event.AjaxRecommendationAcceptedEvent;
import de.tudarmstadt.ukp.inception.recommendation.event.AjaxRecommendationRejectedEvent;
import de.tudarmstadt.ukp.inception.recommendation.event.PredictionsSwitchedEvent;
import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState;
import de.tudarmstadt.ukp.inception.rendering.editorstate.FeatureState;
import de.tudarmstadt.ukp.inception.rendering.pipeline.RenderAnnotationsEvent;
Expand Down Expand Up @@ -701,12 +701,15 @@ private void actionSkip(AjaxRequestTarget aTarget) throws AnnotationException

var sessionOwner = userService.getCurrentUsername();

alStateModel.getObject().getSuggestion().ifPresent(suggestion -> {
requestClearningSelectionAndJumpingToSuggestion();
activeLearningService.skipSpanSuggestion(sessionOwner, getModelObject().getUser(),
alStateModel.getObject().getLayer(), suggestion);
moveToNextSuggestion(aTarget);
});
var maybeSuggestion = alStateModel.getObject().getSuggestion();
if (!maybeSuggestion.isPresent()) {
return;
}

requestClearningSelectionAndJumpingToSuggestion();
activeLearningService.skipSpanSuggestion(sessionOwner, getModelObject().getUser(),
alStateModel.getObject().getLayer(), maybeSuggestion.get());
moveToNextSuggestion(aTarget);
}

private void actionReject(AjaxRequestTarget aTarget) throws AnnotationException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@
title="Delete">
<i class="far fa-times-circle"></i>
</button>
<button wicket:id="accept" type="button"
class="btn-outline-success btn btn-sm py-0 px-1"
title="Accept">
<i class="far fa-check-circle"></i>
<span wicket:id="score" class="small font-monospace text-muted"/>
</button>
<button wicket:id="reject" type="button"
class="btn-outline-danger btn btn-sm py-0 px-1"
title="Reject">
<i class="far fa-times-circle"></i>
</button>
</div>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
*/
package de.tudarmstadt.ukp.inception.ui.core.docanno.sidebar;

import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.EXTENSION_ID;
import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation.MAIN_EDITOR;
import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction.ACCEPTED;
import static de.tudarmstadt.ukp.inception.support.lambda.HtmlElementEvents.CLICK;
import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.enabledWhen;
import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhen;
import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhenNot;
import static java.util.Collections.emptyList;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;
import static org.apache.wicket.event.Broadcast.BREADTH;

import java.io.IOException;
import java.io.Serializable;
Expand All @@ -32,10 +36,13 @@
import java.util.List;
import java.util.Objects;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.uima.UIMAException;
import org.apache.uima.cas.CAS;
import org.apache.wicket.AttributeModifier;
import org.apache.wicket.Component;
import org.apache.wicket.Session;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.feedback.IFeedback;
import org.apache.wicket.markup.html.WebMarkupContainer;
Expand All @@ -59,11 +66,14 @@
import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer;
import de.tudarmstadt.ukp.clarin.webanno.model.Project;
import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument;
import de.tudarmstadt.ukp.clarin.webanno.security.UserDao;
import de.tudarmstadt.ukp.clarin.webanno.security.model.User;
import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage;
import de.tudarmstadt.ukp.inception.annotation.events.FeatureValueUpdatedEvent;
import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler;
import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService;
import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService;
import de.tudarmstadt.ukp.inception.recommendation.api.event.AjaxRecommendationAcceptedEvent;
import de.tudarmstadt.ukp.inception.recommendation.api.model.MetadataSuggestion;
import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState;
import de.tudarmstadt.ukp.inception.rendering.vmodel.VID;
Expand All @@ -89,18 +99,23 @@ public class DocumentMetadataAnnotationSelectionPanel
private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

private static final String CID_LABEL = "label";
private static final String CID_SCORE = "score";
private static final String CID_LAYERS = "layers";
private static final String CID_ANNOTATIONS = "annotations";
private static final String CID_LAYER = "layer";
private static final String CID_CREATE = "create";
private static final String CID_ANNOTATIONS_CONTAINER = "annotationsContainer";
private static final String CID_ANNOTATION_DETAILS = "annotationDetails";
private static final String CID_DELETE = "delete";
private static final String CID_ACCEPT = "accept";
private static final String CID_REJECT = "reject";

private @SpringBean LayerSupportRegistry layerSupportRegistry;
private @SpringBean FeatureSupportRegistry fsRegistry;
private @SpringBean AnnotationSchemaService annotationService;
private @SpringBean RecommendationService recommendationService;
private @SpringBean LearningRecordService learningRecordService;
private @SpringBean UserDao userService;

private final AnnotationPage annotationPage;
private final CasProvider jcasProvider;
Expand Down Expand Up @@ -163,6 +178,44 @@ public DocumentMetadataAnnotationSelectionPanel(String aId, CasProvider aCasProv
add(noLayersWarning);
}

private void actionAccept(AjaxRequestTarget aTarget, AnnotationListItem aItem)
{
try {
var page = (AnnotationPage) aTarget.getPage();
var dataOwner = state.getObject().getUser().getUsername();
var sessionOwner = userService.getCurrentUser();
var suggestion = (MetadataSuggestion) recommendationService
.getPredictions(sessionOwner, state.getObject().getProject())
.getPredictionByVID(state.getObject().getDocument(), aItem.vid).get();
var layer = annotationService.getLayer(suggestion.getLayerId());
var feature = annotationService.getFeature(suggestion.getFeature(), layer);
var adapter = (DocumentMetadataLayerAdapter) annotationService.getAdapter(layer);

var aCas = jcasProvider.get();

var annotation = new DocumentMetadataRecommendationSupport(recommendationService)
.acceptSuggestion(sessionOwner.getUsername(), state.getObject().getDocument(),
dataOwner, aCas, adapter, feature, suggestion, MAIN_EDITOR, ACCEPTED);

page.writeEditorCas(aCas);

// Set selection to the accepted annotation and select it and load it into the detail
// editor
// state.getObject().getSelection().set(adapter.select(VID.of(annotation), annotation));

// Send a UI event that the suggestion has been accepted
page.send(page, BREADTH,
new AjaxRecommendationAcceptedEvent(aTarget, state.getObject(), aItem.vid));
}
catch (Exception e) {
handleException(this, aTarget, e);
}
}

private void actionReject(AjaxRequestTarget a$, AnnotationListItem aAnnotationListItem)
{
}

private void actionCreate(AjaxRequestTarget aTarget) throws AnnotationException, IOException
{
try {
Expand Down Expand Up @@ -297,8 +350,12 @@ protected void populateItem(ListItem<AnnotationListItem> aItem)
Model.of(vid), jcasProvider, annotationPage, actionHandler, state);
aItem.add(detailPanel);

container.add(new LambdaAjaxEventBehavior(CLICK,
$ -> actionSelect($, container, detailPanel)));
var isSuggestion = EXTENSION_ID.equals(aItem.getModelObject().vid.getExtensionId());

if (!isSuggestion) {
container.add(new LambdaAjaxEventBehavior(CLICK,
$ -> actionSelect($, container, detailPanel)));
}

detailPanel.add(visibleWhen(() -> isExpanded(aItem, container)));

Expand All @@ -309,24 +366,42 @@ protected void populateItem(ListItem<AnnotationListItem> aItem)
}

var close = new WebMarkupContainer("close");
close.add(visibleWhen(() -> isExpanded(aItem, container)));
close.add(visibleWhen(() -> isExpanded(aItem, container) && !isSuggestion));
close.setOutputMarkupId(true);
container.add(close);

var open = new WebMarkupContainer("open");
open.add(visibleWhen(() -> !isExpanded(aItem, container)));
open.add(visibleWhen(() -> !isExpanded(aItem, container) && !isSuggestion));
open.setOutputMarkupId(true);
container.add(open);

container.add(new Label(CID_LABEL).add(visibleWhen(
() -> !aItem.getModelObject().singleton && !isExpanded(aItem, container))));
container.setOutputMarkupId(true);
container.add(new Label(CID_LABEL,
StringUtils.defaultIfEmpty(aItem.getModelObject().label, "[No label]"))
.add(visibleWhen(() -> !aItem.getModelObject().singleton
&& !isExpanded(aItem, container))));

aItem.add(new LambdaAjaxLink(CID_DELETE, $ -> actionDelete($, detailPanel))
.add(visibleWhen(() -> !aItem.getModelObject().singleton))
aItem.queue(new LambdaAjaxLink(CID_DELETE, $ -> actionDelete($, detailPanel))
.add(AttributeModifier.replace("title", aItem.getModelObject().vid))
.add(visibleWhen(() -> !aItem.getModelObject().singleton && !isSuggestion))
.add(enabledWhen(() -> annotationPage.isEditable()
&& !aItem.getModelObject().layer.isReadonly())));

aItem.queue(new Label(CID_SCORE, String.format(Session.get().getLocale(), "%.2f",
aItem.getModelObject().score)).add(visibleWhen(
() -> isSuggestion && aItem.getModelObject().score != 0.0d)));

aItem.queue(
new LambdaAjaxLink(CID_ACCEPT, $ -> actionAccept($, aItem.getModelObject()))
.add(AttributeModifier.replace("title", aItem.getModelObject().vid))
.add(visibleWhen(() -> isSuggestion && annotationPage.isEditable()
&& !aItem.getModelObject().layer.isReadonly())));

aItem.queue(
new LambdaAjaxLink(CID_REJECT, $ -> actionReject($, aItem.getModelObject()))
.add(AttributeModifier.replace("title", aItem.getModelObject().vid))
.add(visibleWhen(() -> isSuggestion && annotationPage.isEditable()
&& !aItem.getModelObject().layer.isReadonly())));

aItem.setOutputMarkupId(true);
}

Expand Down Expand Up @@ -393,7 +468,7 @@ private List<AnnotationListItem> listAnnotations(AnnotationLayer aLayer)
for (var fs : cas.select(adapter.getAnnotationType(cas))) {
var renderedFeatures = renderer.renderLabelFeatureValues(adapter, fs, features);
var labelText = TypeUtil.getUiLabelText(renderedFeatures);
items.add(new AnnotationListItem(VID.of(fs), labelText, aLayer, singleton));
items.add(new AnnotationListItem(VID.of(fs), labelText, aLayer, singleton, 0.0d));
}

// --- Populate with predictions ---
Expand All @@ -410,7 +485,7 @@ private List<AnnotationListItem> listAnnotations(AnnotationLayer aLayer)
var annotation = featureSupport.renderFeatureValue(feature,
suggestion.getLabel());
items.add(new AnnotationListItem(suggestion.getVID(), annotation, aLayer,
singleton));
singleton, suggestion.getScore()));
}
}
}
Expand Down Expand Up @@ -486,14 +561,16 @@ private class AnnotationListItem
final String label;
final AnnotationLayer layer;
final boolean singleton;
final double score;

public AnnotationListItem(VID aVid, String aLabel, AnnotationLayer aLayer,
boolean aSingleton)
boolean aSingleton, double aScore)
{
vid = aVid;
label = aLabel;
layer = aLayer;
singleton = aSingleton;
score = aScore;
}
}
}
Loading

0 comments on commit 91180d3

Please sign in to comment.