From f3ab1965cddb4329d9d9ce1122e187de2b09dd5e Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Sun, 4 Feb 2024 17:28:54 +0100 Subject: [PATCH] #4491 - Support for link feature recommendations - Allow recommenders to generate suggestions for link features - Added unit tests --- .../feature/link/LinkFeatureEditor.java | 25 +- .../feature/link/LinkFeatureSupport.java | 23 +- .../layer/relation/RelationAdapter.java | 10 + .../brat/render/BratSerializerImpl.java | 24 +- .../LazyDetailsLookupServiceImpl.java | 5 + .../StringMatchingRelationRecommender.java | 49 ++- .../api/RecommenderTypeSystemUtils.java | 9 +- .../recommendation/api/SuggestionSupport.java | 25 +- .../api/model/ArcPosition_ImplBase.java | 118 +++++++ .../api/model/ArcSuggestion_ImplBase.java | 234 ++++++++++++++ .../api/model/LinkPosition.java | 48 +++ .../api/model/LinkSuggestion.java | 68 ++++ .../api/model/RelationPosition.java | 102 +----- .../api/model/RelationSuggestion.java | 212 +------------ .../RecommendationEditorExtension.java | 76 +---- .../RecommenderServiceAutoConfiguration.java | 16 +- .../link/LinkSuggestionRenderer.java | 65 ++++ .../link/LinkSuggestionSupport.java | 299 ++++++++++++++++++ .../ArcSuggestionRenderer_ImplBase.java | 153 +++++++++ .../ArcSuggestionSupport_ImplBase.java | 189 +++++++++++ .../relation/RelationSuggestionRenderer.java | 114 ++----- .../relation/RelationSuggestionSupport.java | 162 ++-------- .../link/LinkSuggestionExtractionTest.java | 199 ++++++++++++ ...nkSuggestionVisibilityCalculationTest.java | 259 +++++++++++++++ ...onSuggestionVisibilityCalculationTest.java | 31 +- .../recommendation/service/Fixtures.java | 20 -- .../schema/api/adapter/TypeAdapter.java | 9 + .../schema/api/feature/LinkWithRoleModel.java | 9 + .../service/AnnotationSchemaServiceImpl.java | 24 +- 29 files changed, 1877 insertions(+), 700 deletions(-) create mode 100644 inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/ArcPosition_ImplBase.java create mode 100644 inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/ArcSuggestion_ImplBase.java create mode 100644 inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LinkPosition.java create mode 100644 inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LinkSuggestion.java create mode 100644 inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionRenderer.java create mode 100644 inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionSupport.java create mode 100644 inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/ArcSuggestionRenderer_ImplBase.java create mode 100644 inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/ArcSuggestionSupport_ImplBase.java create mode 100644 inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionExtractionTest.java create mode 100644 inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionVisibilityCalculationTest.java diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureEditor.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureEditor.java index f2279a53ee0..b2dd0fd0246 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureEditor.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureEditor.java @@ -563,13 +563,12 @@ private void actionAdd(AjaxRequestTarget aTarget) } @SuppressWarnings("unchecked") - List links = (List) LinkFeatureEditor.this - .getModelObject().value; - AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); + var links = (List) LinkFeatureEditor.this.getModelObject().value; + var state = LinkFeatureEditor.this.stateModel.getObject(); - LinkWithRoleModel m = new LinkWithRoleModel(); + var m = new LinkWithRoleModel(); m.role = (String) field.getModelObject(); - int insertionPoint = findInsertionPoint(links); + var insertionPoint = findInsertionPoint(links); links.add(insertionPoint, m); state.setArmedSlot(getModelObject(), insertionPoint); @@ -600,12 +599,11 @@ private void actionSet(AjaxRequestTarget aTarget) } @SuppressWarnings("unchecked") - List links = (List) LinkFeatureEditor.this - .getModelObject().value; - AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); + var links = (List) LinkFeatureEditor.this.getModelObject().value; + var state = LinkFeatureEditor.this.stateModel.getObject(); // Update the slot - LinkWithRoleModel m = links.get(state.getArmedSlot()); + var m = links.get(state.getArmedSlot()); m.role = (String) field.getModelObject(); links.set(state.getArmedSlot(), m); // avoid reordering @@ -623,11 +621,10 @@ private void actionSet(AjaxRequestTarget aTarget) private void actionDel(AjaxRequestTarget aTarget) { @SuppressWarnings("unchecked") - List links = (List) LinkFeatureEditor.this - .getModelObject().value; - AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); + var links = (List) LinkFeatureEditor.this.getModelObject().value; + var state = LinkFeatureEditor.this.stateModel.getObject(); - LinkWithRoleModel linkWithRoleModel = links.get(state.getArmedSlot()); + var linkWithRoleModel = links.get(state.getArmedSlot()); links.remove(state.getArmedSlot()); state.clearArmedSlot(); @@ -638,7 +635,7 @@ private void actionDel(AjaxRequestTarget aTarget) private void actionToggleArmedState(AjaxRequestTarget aTarget, Item aItem) { - AnnotatorState state = LinkFeatureEditor.this.stateModel.getObject(); + var state = LinkFeatureEditor.this.stateModel.getObject(); if (state.isArmedSlot(getModelObject(), aItem.getIndex())) { state.clearArmedSlot(); diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupport.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupport.java index ce91be4fdae..ce4042b39aa 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupport.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/feature/link/LinkFeatureSupport.java @@ -45,6 +45,9 @@ import de.tudarmstadt.ukp.clarin.webanno.model.Tag; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; +import de.tudarmstadt.ukp.inception.annotation.layer.chain.ChainLayerSupport; +import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.editorstate.FeatureState; @@ -54,7 +57,6 @@ import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupport; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureType; import de.tudarmstadt.ukp.inception.schema.api.feature.LinkWithRoleModel; -import de.tudarmstadt.ukp.inception.support.WebAnnoConst; import de.tudarmstadt.ukp.inception.support.json.JSONUtil; import de.tudarmstadt.ukp.inception.support.uima.ICasUtil; @@ -92,23 +94,22 @@ public void setBeanName(String aBeanName) } @Override - public List getSupportedFeatureTypes(AnnotationLayer aAnnotationLayer) + public List getSupportedFeatureTypes(AnnotationLayer aLayer) { - List types = new ArrayList<>(); + var types = new ArrayList(); // Slot features are only supported on span layers - if (!WebAnnoConst.CHAIN_TYPE.equals(aAnnotationLayer.getType()) - && !WebAnnoConst.RELATION_TYPE.equals(aAnnotationLayer.getType())) { + if (!ChainLayerSupport.TYPE.equals(aLayer.getType()) + && !RelationLayerSupport.TYPE.equals(aLayer.getType())) { // Add layers of type SPAN available in the project - for (AnnotationLayer spanLayer : annotationService - .listAnnotationLayer(aAnnotationLayer.getProject())) { + for (var spanLayer : annotationService.listAnnotationLayer(aLayer.getProject())) { if (Token.class.getName().equals(spanLayer.getName()) || Sentence.class.getName().equals(spanLayer.getName())) { continue; } - if (WebAnnoConst.SPAN_TYPE.equals(spanLayer.getType())) { + if (SpanLayerSupport.TYPE.equals(spanLayer.getType())) { types.add(new FeatureType(spanLayer.getName(), "Link: " + spanLayer.getUiName(), featureSupportId)); } @@ -189,7 +190,7 @@ public void generateFeature(TypeSystemDescription aTSD, TypeDescription aTD, AnnotationFeature aFeature) { // Link type - TypeDescription linkTD = aTSD.addType(aFeature.getLinkTypeName(), "", CAS.TYPE_NAME_TOP); + var linkTD = aTSD.addType(aFeature.getLinkTypeName(), "", CAS.TYPE_NAME_TOP); linkTD.addFeature(aFeature.getLinkTypeRoleFeatureName(), "", CAS.TYPE_NAME_STRING); linkTD.addFeature(aFeature.getLinkTypeTargetFeatureName(), "", aFeature.getType()); @@ -201,7 +202,7 @@ public void generateFeature(TypeSystemDescription aTSD, TypeDescription aTD, @Override public List getFeatureValue(AnnotationFeature aFeature, FeatureStructure aFS) { - Feature linkFeature = aFS.getType().getFeatureByBaseName(aFeature.getName()); + var linkFeature = aFS.getType().getFeatureByBaseName(aFeature.getName()); if (linkFeature == null) { wrapFeatureValue(aFeature, aFS.getCAS(), null); @@ -215,7 +216,7 @@ public void setFeatureValue(CAS aCas, AnnotationFeature aFeature, int aAddress, throws AnnotationException { if (aValue instanceof List && aFeature.getTagset() != null) { - for (LinkWithRoleModel link : (List) aValue) { + for (var link : (List) aValue) { if (!annotationService.existsTag(link.role, aFeature.getTagset())) { if (!aFeature.getTagset().isCreateTag()) { throw new IllegalArgumentException("[" + link.role diff --git a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationAdapter.java b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationAdapter.java index 00ba6389e46..f5bb934132c 100644 --- a/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationAdapter.java +++ b/inception/inception-api-annotation/src/main/java/de/tudarmstadt/ukp/inception/annotation/layer/relation/RelationAdapter.java @@ -203,11 +203,21 @@ public String getSourceFeatureName() return sourceFeatureName; } + public Feature getSourceFeature(CAS aCas) + { + return getAnnotationType(aCas).getFeatureByBaseName(getSourceFeatureName()); + } + public String getTargetFeatureName() { return targetFeatureName; } + public Feature getTargetFeature(CAS aCas) + { + return getAnnotationType(aCas).getFeatureByBaseName(getTargetFeatureName()); + } + @Override public List> validate(CAS aCas) { diff --git a/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java b/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java index 2eb98b86956..d0eaee0c547 100644 --- a/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java +++ b/inception/inception-brat-editor/src/main/java/de/tudarmstadt/ukp/clarin/webanno/brat/render/BratSerializerImpl.java @@ -20,7 +20,6 @@ import static de.tudarmstadt.ukp.clarin.webanno.brat.schema.BratSchemaGeneratorImpl.getBratTypeName; import static de.tudarmstadt.ukp.clarin.webanno.model.ScriptDirection.RTL; import static java.util.Arrays.asList; -import static java.util.stream.Collectors.toList; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.uima.fit.util.CasUtil.getType; import static org.apache.uima.fit.util.CasUtil.select; @@ -52,19 +51,16 @@ import de.tudarmstadt.ukp.clarin.webanno.brat.render.model.SentenceComment; import de.tudarmstadt.ukp.clarin.webanno.brat.render.model.SentenceMarker; import de.tudarmstadt.ukp.clarin.webanno.brat.render.model.TextMarker; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.TrimUtils; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.inception.rendering.paging.Unit; import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; import de.tudarmstadt.ukp.inception.rendering.vmodel.VAnnotationMarker; -import de.tudarmstadt.ukp.inception.rendering.vmodel.VArc; import de.tudarmstadt.ukp.inception.rendering.vmodel.VComment; import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; import de.tudarmstadt.ukp.inception.rendering.vmodel.VMarker; import de.tudarmstadt.ukp.inception.rendering.vmodel.VSentenceMarker; -import de.tudarmstadt.ukp.inception.rendering.vmodel.VSpan; import de.tudarmstadt.ukp.inception.rendering.vmodel.VTextMarker; import de.tudarmstadt.ukp.inception.support.text.TextUtils; import de.tudarmstadt.ukp.inception.support.uima.ICasUtil; @@ -100,7 +96,7 @@ public String getId() @Override public GetDocumentResponse render(VDocument aVDoc, RenderRequest aRequest) { - GetDocumentResponse aResponse = new GetDocumentResponse(); + var aResponse = new GetDocumentResponse(); aResponse.setRtlMode(RTL == aRequest.getState().getScriptDirection()); aResponse.setFontZoom(aRequest.getState().getPreferences().getFontZoom()); aResponse.setWindowBegin(aVDoc.getWindowBegin()); @@ -125,9 +121,9 @@ public GetDocumentResponse render(VDocument aVDoc, RenderRequest aRequest) private void renderLayers(GetDocumentResponse aResponse, VDocument aVDoc) { - for (AnnotationLayer layer : aVDoc.getAnnotationLayers()) { - for (VSpan vspan : aVDoc.spans(layer.getId())) { - List offsets = vspan.getRanges().stream() // + for (var layer : aVDoc.getAnnotationLayers()) { + for (var vspan : aVDoc.spans(layer.getId())) { + var offsets = vspan.getRanges().stream() // .flatMap(range -> split(aResponse.getSentenceOffsets(), aVDoc.getText(), aVDoc.getWindowBegin(), range.getBegin(), range.getEnd()).stream()) .map(range -> { @@ -137,11 +133,10 @@ private void renderLayers(GetDocumentResponse aResponse, VDocument aVDoc) range.setEnd(span[1]); return range; }) // - .collect(toList()); + .toList(); - Entity entity = new Entity(vspan.getVid(), getBratTypeName(vspan.getLayer()), - offsets, vspan.getLabelHint(), vspan.getColorHint(), - vspan.isActionButtons()); + var entity = new Entity(vspan.getVid(), getBratTypeName(vspan.getLayer()), offsets, + vspan.getLabelHint(), vspan.getColorHint(), vspan.isActionButtons()); if (!layer.isShowTextInHover()) { // If the layer is configured not to display the span text in the popup, then // we simply set the popup to the empty string here. @@ -160,9 +155,8 @@ private void renderLayers(GetDocumentResponse aResponse, VDocument aVDoc) aResponse.addEntity(entity); } - for (VArc varc : aVDoc.arcs(layer.getId())) { - - Relation arc = new Relation(varc.getVid(), getBratTypeName(varc.getLayer()), + for (var varc : aVDoc.arcs(layer.getId())) { + var arc = new Relation(varc.getVid(), getBratTypeName(varc.getLayer()), getArgument(varc.getSource(), varc.getTarget()), varc.getLabelHint(), varc.getColorHint()); aResponse.addRelation(arc); diff --git a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupServiceImpl.java b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupServiceImpl.java index a79a8eca4bf..492e5664c1a 100644 --- a/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupServiceImpl.java +++ b/inception/inception-diam/src/main/java/de/tudarmstadt/ukp/inception/diam/editor/lazydetails/LazyDetailsLookupServiceImpl.java @@ -35,6 +35,7 @@ import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.LinkMode; import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; @@ -186,6 +187,10 @@ private List lookupExtensionLevelDetails(VID aVid, SourceDocum return emptyList(); } + if (aFeature.getLinkMode() == LinkMode.WITH_ROLE) { + return emptyList(); + } + var result = new ArrayList(); var extension = extensionRegistry.getExtension(aVid.getExtensionId()); var value = extension.getFeatureValue(aDocument, aUser, aCas, aVid, aFeature); diff --git a/inception/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/relation/StringMatchingRelationRecommender.java b/inception/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/relation/StringMatchingRelationRecommender.java index 8fc35f0f5ba..61420a4f07b 100644 --- a/inception/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/relation/StringMatchingRelationRecommender.java +++ b/inception/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/relation/StringMatchingRelationRecommender.java @@ -32,7 +32,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.Objects; import org.apache.commons.collections4.MultiValuedMap; @@ -87,8 +86,8 @@ private MultiValuedMap, String> trainModel(List aDa { MultiValuedMap, String> model = new ArrayListValuedHashMap<>(); - for (Triple t : aData) { - Pair key = Pair.of(t.governor, t.dependent); + for (var t : aData) { + var key = Pair.of(t.governor, t.dependent); model.put(key, t.label); } @@ -102,45 +101,45 @@ public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) MultiValuedMap, String> model = aContext.get(KEY_MODEL).orElseThrow( () -> new RecommendationException("Key [" + KEY_MODEL + "] not found in context")); - Type sampleUnitType = getType(aCas, SAMPLE_UNIT); + var sampleUnitType = getType(aCas, SAMPLE_UNIT); - Type predictedType = getPredictedType(aCas); - Feature governorFeature = predictedType.getFeatureByBaseName(FEAT_REL_SOURCE); - Feature dependentFeature = predictedType.getFeatureByBaseName(FEAT_REL_TARGET); - Feature predictedFeature = getPredictedFeature(aCas); - Feature isPredictionFeature = getIsPredictionFeature(aCas); - Type attachType = getAttachType(aCas); - Feature attachFeature = getAttachFeature(aCas); - Feature scoreFeature = getScoreFeature(aCas); + var predictedType = getPredictedType(aCas); + var governorFeature = predictedType.getFeatureByBaseName(FEAT_REL_SOURCE); + var dependentFeature = predictedType.getFeatureByBaseName(FEAT_REL_TARGET); + var predictedFeature = getPredictedFeature(aCas); + var isPredictionFeature = getIsPredictionFeature(aCas); + var attachType = getAttachType(aCas); + var attachFeature = getAttachFeature(aCas); + var scoreFeature = getScoreFeature(aCas); // Relations are predicted only within the sample units - thus instead of looking at the // whole document for potential relations, we only need to look at those units that overlap // with the current prediction request area var units = selectOverlapping(aCas, sampleUnitType, aBegin, aEnd); - for (AnnotationFS sampleUnit : units) { + for (var sampleUnit : units) { Collection baseAnnotations = selectCovered(attachType, sampleUnit); - for (AnnotationFS governor : baseAnnotations) { - for (AnnotationFS dependent : baseAnnotations) { + for (var governor : baseAnnotations) { + for (var dependent : baseAnnotations) { if (governor.equals(dependent)) { continue; } - String governorLabel = governor.getStringValue(attachFeature); - String dependentLabel = dependent.getStringValue(attachFeature); + var governorLabel = governor.getStringValue(attachFeature); + var dependentLabel = dependent.getStringValue(attachFeature); - Pair key = Pair.of(governorLabel, dependentLabel); - Collection occurrences = model.get(key); - Map numberOfOccurrencesPerLabel = occurrences.stream() // + var key = Pair.of(governorLabel, dependentLabel); + var occurrences = model.get(key); + var numberOfOccurrencesPerLabel = occurrences.stream() // .collect(groupingBy(identity(), counting())); - double totalNumberOfOccurrences = occurrences.size(); + var totalNumberOfOccurrences = occurrences.size(); - for (String relationLabel : occurrences) { - double score = numberOfOccurrencesPerLabel.get(relationLabel) + for (var relationLabel : occurrences) { + var score = numberOfOccurrencesPerLabel.get(relationLabel) / totalNumberOfOccurrences; - AnnotationFS prediction = aCas.createAnnotation(predictedType, - governor.getBegin(), governor.getEnd()); + var prediction = aCas.createAnnotation(predictedType, governor.getBegin(), + governor.getEnd()); prediction.setFeatureValue(governorFeature, governor); prediction.setFeatureValue(dependentFeature, dependent); prediction.setStringValue(predictedFeature, relationLabel); diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommenderTypeSystemUtils.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommenderTypeSystemUtils.java index 1cd8a92d23d..792580c376d 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommenderTypeSystemUtils.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommenderTypeSystemUtils.java @@ -22,7 +22,6 @@ import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_SCORE_EXPLANATION_SUFFIX; import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_SCORE_SUFFIX; import static java.util.Arrays.asList; -import static java.util.stream.Collectors.toList; import static org.apache.uima.cas.CAS.TYPE_NAME_BOOLEAN; import static org.apache.uima.cas.CAS.TYPE_NAME_DOUBLE; import static org.apache.uima.cas.CAS.TYPE_NAME_STRING; @@ -34,13 +33,13 @@ import org.apache.uima.fit.factory.CasFactory; import org.apache.uima.resource.ResourceInitializationException; import org.apache.uima.resource.metadata.TypeSystemDescription; +import org.apache.uima.util.CasCopier; import org.apache.uima.util.TypeSystemUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.inception.support.WebAnnoConst; -import de.tudarmstadt.ukp.inception.support.uima.SegmentationUtils; public class RecommenderTypeSystemUtils { @@ -53,8 +52,7 @@ public static CAS makePredictionCas(CAS aOriginalCas, AnnotationFeature... aFeat RecommenderTypeSystemUtils.addPredictionFeaturesToTypeSystem(tsd, asList(aFeatures)); var predictionCas = CasFactory.createCas(tsd); predictionCas.setDocumentText(aOriginalCas.getDocumentText()); - SegmentationUtils.splitSentences(predictionCas); - SegmentationUtils.tokenize(predictionCas); + CasCopier.copyCas(aOriginalCas, predictionCas, false); return predictionCas; } @@ -83,8 +81,7 @@ public static void addPredictionFeaturesToTypeSystem(TypeSystemDescription tsd, td.addFeature(modeFeatureName, "Suggestion mode", TYPE_NAME_STRING); } - var layers = features.stream().map(AnnotationFeature::getLayer).distinct() - .collect(toList()); + var layers = features.stream().map(AnnotationFeature::getLayer).distinct().toList(); for (var layer : layers) { var td = tsd.getType(layer.getName()); if (td == null) { diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport.java index f6b4bbe6661..01e772952ba 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport.java @@ -63,13 +63,30 @@ void calculateSuggestionVisibility(String aSess Collection> aRecommendations, int aWindowBegin, int aWindowEnd); /** - * Accept the given suggestion. + * Uses the given annotation suggestion to create a new annotation or to update a feature in an + * existing annotation. * + * @param aSessionOwner + * the user currently logged in + * @param aDocument + * the source document to which the annotations belong + * @param aDataOwner + * the annotator user to whom the annotations belong + * @param aCas + * the CAS containing the annotations + * @param aAdapter + * an adapter for the layer to upsert + * @param aFeature + * the feature on the layer that should be upserted * @param aSuggestion - * the suggestion to accept. - * @return the annotation created from the suggestion. + * the suggestion + * @param aLocation + * the location from where the change was triggered + * @param aAction + * whether the annotation was accepted or corrected + * @return the created/updated annotation. * @throws AnnotationException - * if there was a problem creating the annotation. + * if there was an annotation-level problem */ AnnotationBaseFS acceptSuggestion(String aSessionOwner, SourceDocument aDocument, String aDataOwner, CAS aCas, TypeAdapter aAdapter, AnnotationFeature aFeature, diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/ArcPosition_ImplBase.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/ArcPosition_ImplBase.java new file mode 100644 index 00000000000..44f6e0583aa --- /dev/null +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/ArcPosition_ImplBase.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.api.model; + +import java.io.Serializable; +import java.util.Objects; + +import org.apache.commons.lang3.builder.CompareToBuilder; +import org.apache.uima.cas.text.AnnotationFS; + +public abstract class ArcPosition_ImplBase> + implements Serializable, Position, Comparable +{ + private static final long serialVersionUID = 6630083774056904670L; + + protected final int sourceBegin; + protected final int sourceEnd; + protected final int targetBegin; + protected final int targetEnd; + + public ArcPosition_ImplBase(AnnotationFS aSource, AnnotationFS aTarget) + { + sourceBegin = aSource.getBegin(); + sourceEnd = aSource.getEnd(); + targetBegin = aTarget.getBegin(); + targetEnd = aTarget.getEnd(); + } + + public ArcPosition_ImplBase(int aSourceBegin, int aSourceEnd, int aTargetBegin, int aTargetEnd) + { + sourceBegin = aSourceBegin; + sourceEnd = aSourceEnd; + targetBegin = aTargetBegin; + targetEnd = aTargetEnd; + } + + public ArcPosition_ImplBase(T aOther) + { + sourceBegin = aOther.sourceBegin; + sourceEnd = aOther.sourceEnd; + targetBegin = aOther.targetBegin; + targetEnd = aOther.targetEnd; + } + + @Override + public String toString() + { + + return String.format("RelationPosition{(%d, %d) -> (%d, %d)}", sourceBegin, sourceEnd, + targetBegin, targetEnd); + } + + public int getSourceBegin() + { + return sourceBegin; + } + + public int getSourceEnd() + { + return sourceEnd; + } + + public int getTargetBegin() + { + return targetBegin; + } + + public int getTargetEnd() + { + return targetEnd; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ArcPosition_ImplBase that = (ArcPosition_ImplBase) o; + return sourceBegin == that.sourceBegin && sourceEnd == that.sourceEnd + && targetBegin == that.targetBegin && targetEnd == that.targetEnd; + } + + @Override + public int hashCode() + { + return Objects.hash(sourceBegin, sourceEnd, targetBegin, targetEnd); + } + + @Override + public int compareTo(T o) + { + return new CompareToBuilder() // + .append(getSourceBegin(), o.getSourceBegin()) // + .append(getSourceEnd(), o.getSourceEnd()) // + .append(getTargetBegin(), o.getTargetBegin()) // + .append(getTargetEnd(), o.getTargetEnd()).toComparison(); + } + +} diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/ArcSuggestion_ImplBase.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/ArcSuggestion_ImplBase.java new file mode 100644 index 00000000000..c07a22be4ee --- /dev/null +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/ArcSuggestion_ImplBase.java @@ -0,0 +1,234 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.api.model; + +import java.io.Serializable; + +import org.apache.commons.lang3.builder.ToStringBuilder; + +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; + +public abstract class ArcSuggestion_ImplBase

> + extends AnnotationSuggestion + implements Serializable +{ + private static final long serialVersionUID = -4873732473868120957L; + + protected final P position; + + protected ArcSuggestion_ImplBase(Builder builder) + { + super(builder.id, builder.generation, builder.age, builder.recommenderId, + builder.recommenderName, builder.layerId, builder.feature, builder.documentName, + builder.label, builder.uiLabel, builder.score, builder.scoreExplanation, + builder.autoAcceptMode, builder.hidingFlags); + + this.position = builder.position; + } + + // Getter and setter + + @Override + public P getPosition() + { + return position; + } + + // The begin of the window is min(source.begin, target.begin) + // The end of the window is max(source.end, target.end) + // This is mostly used to optimize the viewport when rendering + + @Override + public int getWindowBegin() + { + return Math.min(position.getSourceBegin(), position.getTargetBegin()); + } + + @Override + public int getWindowEnd() + { + return Math.max(position.getSourceEnd(), position.getTargetEnd()); + } + + @Override + public String toString() + { + return new ToStringBuilder(this) // + .append("id", id) // + .append("generation", generation) // + .append("age", getAge()) // + .append("recommenderId", recommenderId) // + .append("recommenderName", recommenderName) // + .append("layerId", layerId) // + .append("feature", feature) // + .append("documentName", documentName) // + .append("position", position) // + .append("windowBegin", getWindowBegin()) // + .append("windowEnd", getWindowEnd()) // + .append("label", label) // + .append("uiLabel", uiLabel) // + .append("score", score) // + .append("confindenceExplanation", scoreExplanation) // + .append("visible", isVisible()) // + .append("reasonForHiding", getReasonForHiding()) // + .toString(); + } + + @Override + public ArcSuggestion_ImplBase assignId(int aId) + { + return toBuilder().withId(aId).build(); + } + + public abstract Builder toBuilder(); + + public static abstract class Builder, P extends ArcPosition_ImplBase> + { + protected int generation; + protected int age; + protected int id; + protected long recommenderId; + protected String recommenderName; + protected long layerId; + protected String feature; + protected String documentName; + protected String label; + protected String uiLabel; + protected double score; + protected String scoreExplanation; + protected P position; + protected AutoAcceptMode autoAcceptMode; + protected int hidingFlags; + + protected Builder() + { + } + + public T withId(int aId) + { + this.id = aId; + return (T) this; + } + + public T withGeneration(int aGeneration) + { + this.generation = aGeneration; + return (T) this; + } + + public T withAge(int aAge) + { + this.age = aAge; + return (T) this; + } + + public T withRecommender(Recommender aRecommender) + { + this.recommenderId = aRecommender.getId(); + this.recommenderName = aRecommender.getName(); + this.feature = aRecommender.getFeature().getName(); + this.layerId = aRecommender.getLayer().getId(); + return (T) this; + } + + @Deprecated + T withRecommenderId(long aRecommenderId) + { + this.recommenderId = aRecommenderId; + return (T) this; + } + + @Deprecated + T withRecommenderName(String aRecommenderName) + { + this.recommenderName = aRecommenderName; + return (T) this; + } + + @Deprecated + T withLayerId(long aLayerId) + { + this.layerId = aLayerId; + return (T) this; + } + + @Deprecated + T withFeature(String aFeature) + { + this.feature = aFeature; + return (T) this; + } + + public T withDocument(SourceDocument aDocument) + { + this.documentName = aDocument.getName(); + return (T) this; + } + + @Deprecated + public T withDocumentName(String aDocumentName) + { + this.documentName = aDocumentName; + return (T) this; + } + + public T withLabel(String aLabel) + { + this.label = aLabel; + return (T) this; + } + + public T withUiLabel(String aUiLabel) + { + this.uiLabel = aUiLabel; + return (T) this; + } + + public T withScore(double aScore) + { + this.score = aScore; + return (T) this; + } + + public T withScoreExplanation(String aScoreExplanation) + { + this.scoreExplanation = aScoreExplanation; + return (T) this; + } + + public T withPosition(P aPosition) + { + this.position = aPosition; + return (T) this; + } + + public T withAutoAcceptMode(AutoAcceptMode aAutoAcceptMode) + { + this.autoAcceptMode = aAutoAcceptMode; + return (T) this; + } + + public T withHidingFlags(int aFlags) + { + this.hidingFlags = aFlags; + return (T) this; + } + + public abstract ArcSuggestion_ImplBase build(); + } +} diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LinkPosition.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LinkPosition.java new file mode 100644 index 00000000000..9c3f43520d8 --- /dev/null +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LinkPosition.java @@ -0,0 +1,48 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.api.model; + +import org.apache.uima.cas.text.AnnotationFS; + +public class LinkPosition + extends ArcPosition_ImplBase +{ + private static final long serialVersionUID = 4899546086036031468L; + + private final String feature; + + public LinkPosition(String aFeature, AnnotationFS aSource, AnnotationFS aTarget) + { + super(aSource, aTarget); + feature = aFeature; + } + + public LinkPosition(String aFeature, int aSourceBegin, int aSourceEnd, int aTargetBegin, + int aTargetEnd) + { + super(aSourceBegin, aSourceEnd, aTargetBegin, aTargetEnd); + feature = aFeature; + } + + public LinkPosition(LinkPosition aOther) + { + super(aOther); + feature = aOther.feature; + } + +} diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LinkSuggestion.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LinkSuggestion.java new file mode 100644 index 00000000000..5e5facc367e --- /dev/null +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LinkSuggestion.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.api.model; + +import java.io.Serializable; + +public class LinkSuggestion + extends ArcSuggestion_ImplBase + implements Serializable +{ + private static final long serialVersionUID = -2724884621704905935L; + + public LinkSuggestion(Builder> aBuilder) + { + super(aBuilder); + } + + @Override + public Builder toBuilder() + { + return builder() // + .withId(id) // + .withGeneration(generation) // + .withAge(getAge()) // + .withRecommenderId(recommenderId) // + .withRecommenderName(recommenderName) // + .withLayerId(layerId) // + .withFeature(feature) // + .withDocumentName(documentName) // + .withLabel(label) // + .withUiLabel(uiLabel) // + .withScore(score) // + .withScoreExplanation(scoreExplanation) // + .withPosition(position) // + .withAutoAcceptMode(getAutoAcceptMode()) // + .withHidingFlags(getHidingFlags()); + } + + public static Builder> builder() + { + return new Builder<>(); + } + + public static class Builder> + extends ArcSuggestion_ImplBase.Builder + { + @Override + public LinkSuggestion build() + { + return new LinkSuggestion(this); + } + } +} diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RelationPosition.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RelationPosition.java index a53b7baebca..bdd997026a5 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RelationPosition.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RelationPosition.java @@ -17,119 +17,25 @@ */ package de.tudarmstadt.ukp.inception.recommendation.api.model; -import java.io.Serializable; -import java.util.Objects; - -import org.apache.commons.lang3.builder.CompareToBuilder; import org.apache.uima.cas.text.AnnotationFS; public class RelationPosition - implements Serializable, Position, Comparable + extends ArcPosition_ImplBase { private static final long serialVersionUID = -3084534351646334021L; - private final int sourceBegin; - private final int sourceEnd; - private final int targetBegin; - private final int targetEnd; - public RelationPosition(AnnotationFS aSource, AnnotationFS aTarget) { - sourceBegin = aSource.getBegin(); - sourceEnd = aSource.getEnd(); - targetBegin = aTarget.getBegin(); - targetEnd = aTarget.getEnd(); + super(aSource, aTarget); } public RelationPosition(int aSourceBegin, int aSourceEnd, int aTargetBegin, int aTargetEnd) { - sourceBegin = aSourceBegin; - sourceEnd = aSourceEnd; - targetBegin = aTargetBegin; - targetEnd = aTargetEnd; + super(aSourceBegin, aSourceEnd, aTargetBegin, aTargetEnd); } public RelationPosition(RelationPosition aOther) { - sourceBegin = aOther.sourceBegin; - sourceEnd = aOther.sourceEnd; - targetBegin = aOther.targetBegin; - targetEnd = aOther.targetEnd; - } - - @Override - public String toString() - { - - return String.format("RelationPosition{(%d, %d) -> (%d, %d)}", sourceBegin, sourceEnd, - targetBegin, targetEnd); - } - - public int getSourceBegin() - { - return sourceBegin; - } - - public int getSourceEnd() - { - return sourceEnd; - } - - public int getTargetBegin() - { - return targetBegin; - } - - public int getTargetEnd() - { - return targetEnd; - } - - public boolean overlaps(final RelationPosition i) - { - throw new UnsupportedOperationException("Not implemented yet"); - // // Cases: - // // - // // start end - // // | | - // // 1 ####### | - // // 2 | ####### - // // 3 #################################### - // // 4 | ####### | - // // | | - // - // return (((i.getStart() <= getStart()) && (getStart() < i.getEnd())) || // Case 1-3 - // ((i.getStart() < getEnd()) && (getEnd() <= i.getEnd())) || // Case 1-3 - // ((getStart() <= i.getStart()) && (i.getEnd() <= getEnd()))); // Case 4 - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - RelationPosition that = (RelationPosition) o; - return sourceBegin == that.sourceBegin && sourceEnd == that.sourceEnd - && targetBegin == that.targetBegin && targetEnd == that.targetEnd; - } - - @Override - public int hashCode() - { - return Objects.hash(sourceBegin, sourceEnd, targetBegin, targetEnd); - } - - @Override - public int compareTo(RelationPosition o) - { - return new CompareToBuilder() // - .append(getSourceBegin(), o.getSourceBegin()) // - .append(getSourceEnd(), o.getSourceEnd()) // - .append(getTargetBegin(), o.getTargetBegin()) // - .append(getTargetEnd(), o.getTargetEnd()).toComparison(); + super(aOther); } } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RelationSuggestion.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RelationSuggestion.java index bf4602f917d..70e5c715678 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RelationSuggestion.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/RelationSuggestion.java @@ -19,87 +19,18 @@ import java.io.Serializable; -import org.apache.commons.lang3.builder.ToStringBuilder; - -import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; - public class RelationSuggestion - extends AnnotationSuggestion + extends ArcSuggestion_ImplBase implements Serializable { private static final long serialVersionUID = -1904645143661843249L; - private final RelationPosition position; - - private RelationSuggestion(Builder builder) - { - super(builder.id, builder.generation, builder.age, builder.recommenderId, - builder.recommenderName, builder.layerId, builder.feature, builder.documentName, - builder.label, builder.uiLabel, builder.score, builder.scoreExplanation, - builder.autoAcceptMode, builder.hidingFlags); - - this.position = builder.position; - } - - // Getter and setter - - @Override - public RelationPosition getPosition() - { - return position; - } - - // The begin of the window is min(source.begin, target.begin) - // The end of the window is max(source.end, target.end) - // This is mostly used to optimize the viewport when rendering - - @Override - public int getWindowBegin() - { - return Math.min(position.getSourceBegin(), position.getTargetBegin()); - } - - @Override - public int getWindowEnd() - { - return Math.max(position.getSourceEnd(), position.getTargetEnd()); - } - - @Override - public String toString() + public RelationSuggestion(Builder> aBuilder) { - return new ToStringBuilder(this) // - .append("id", id) // - .append("generation", generation) // - .append("age", getAge()) // - .append("recommenderId", recommenderId) // - .append("recommenderName", recommenderName) // - .append("layerId", layerId) // - .append("feature", feature) // - .append("documentName", documentName) // - .append("position", position) // - .append("windowBegin", getWindowBegin()) // - .append("windowEnd", getWindowEnd()) // - .append("label", label) // - .append("uiLabel", uiLabel) // - .append("score", score) // - .append("confindenceExplanation", scoreExplanation) // - .append("visible", isVisible()) // - .append("reasonForHiding", getReasonForHiding()) // - .toString(); + super(aBuilder); } @Override - public AnnotationSuggestion assignId(int aId) - { - return toBuilder().withId(aId).build(); - } - - public static Builder builder() - { - return new Builder(); - } - public Builder toBuilder() { return builder() // @@ -120,138 +51,15 @@ public Builder toBuilder() .withHidingFlags(getHidingFlags()); } - public static final class Builder + public static Builder> builder() { - private int generation; - private int age; - private int id; - private long recommenderId; - private String recommenderName; - private long layerId; - private String feature; - private String documentName; - private String label; - private String uiLabel; - private double score; - private String scoreExplanation; - private RelationPosition position; - private AutoAcceptMode autoAcceptMode; - private int hidingFlags; - - private Builder() - { - } - - public Builder withId(int aId) - { - this.id = aId; - return this; - } - - public Builder withGeneration(int aGeneration) - { - this.generation = aGeneration; - return this; - } - - public Builder withAge(int aAge) - { - this.age = aAge; - return this; - } - - public Builder withRecommender(Recommender aRecommender) - { - this.recommenderId = aRecommender.getId(); - this.recommenderName = aRecommender.getName(); - this.feature = aRecommender.getFeature().getName(); - this.layerId = aRecommender.getLayer().getId(); - return this; - } - - @Deprecated - Builder withRecommenderId(long aRecommenderId) - { - this.recommenderId = aRecommenderId; - return this; - } - - @Deprecated - Builder withRecommenderName(String aRecommenderName) - { - this.recommenderName = aRecommenderName; - return this; - } - - @Deprecated - Builder withLayerId(long aLayerId) - { - this.layerId = aLayerId; - return this; - } - - @Deprecated - Builder withFeature(String aFeature) - { - this.feature = aFeature; - return this; - } - - public Builder withDocument(SourceDocument aDocument) - { - this.documentName = aDocument.getName(); - return this; - } - - @Deprecated - public Builder withDocumentName(String aDocumentName) - { - this.documentName = aDocumentName; - return this; - } - - public Builder withLabel(String aLabel) - { - this.label = aLabel; - return this; - } - - public Builder withUiLabel(String aUiLabel) - { - this.uiLabel = aUiLabel; - return this; - } - - public Builder withScore(double aScore) - { - this.score = aScore; - return this; - } - - public Builder withScoreExplanation(String aScoreExplanation) - { - this.scoreExplanation = aScoreExplanation; - return this; - } - - public Builder withPosition(RelationPosition aPosition) - { - this.position = aPosition; - return this; - } - - public Builder withAutoAcceptMode(AutoAcceptMode aAutoAcceptMode) - { - this.autoAcceptMode = aAutoAcceptMode; - return this; - } - - public Builder withHidingFlags(int aFlags) - { - this.hidingFlags = aFlags; - return this; - } + return new Builder<>(); + } + public static class Builder> + extends ArcSuggestion_ImplBase.Builder + { + @Override public RelationSuggestion build() { return new RelationSuggestion(this); diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java index 961c06a3a5a..7db2a572743 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/RecommendationEditorExtension.java @@ -50,8 +50,6 @@ 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.layer.relation.RelationAdapter; -import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; import de.tudarmstadt.ukp.inception.diam.editor.actions.ScrollToHandler; import de.tudarmstadt.ukp.inception.diam.editor.actions.SelectAnnotationHandler; import de.tudarmstadt.ukp.inception.editor.AnnotationEditorExtension; @@ -173,32 +171,6 @@ else if (ScrollToHandler.COMMAND.equals(aAction)) { } } - private void actionAcceptPrediction(AnnotationActionHandler aActionHandler, - AnnotatorState aState, AjaxRequestTarget aTarget, CAS aCas, VID aVID, - Optional prediction, SourceDocument document) - throws AnnotationException, IOException - { - if (prediction.map(p -> p instanceof SpanSuggestion).get()) { - actionAcceptSpanRecommendation(aTarget, (SpanSuggestion) prediction.get(), document, - aActionHandler, aState, aCas, aVID); - } - - if (prediction.map(p -> p instanceof RelationSuggestion).get()) { - actionAcceptRelationRecommendation(aTarget, (RelationSuggestion) prediction.get(), - document, aActionHandler, aState, aCas, aVID); - } - } - - private Optional getPrediction(AnnotatorState aState, VID aRecVid) - { - Predictions predictions = recommendationService.getPredictions(aState.getUser(), - aState.getProject()); - SourceDocument document = aState.getDocument(); - Optional prediction = predictions // - .getPredictionByVID(document, aRecVid); - return prediction; - } - /** * Accept a suggestion. * @@ -209,52 +181,38 @@ private Optional getPrediction(AnnotatorState aState, VID *

  • Sends events to the UI and application informing other components about the action.
  • * */ - private void actionAcceptSpanRecommendation(AjaxRequestTarget aTarget, - SpanSuggestion aSuggestion, SourceDocument aSocument, - AnnotationActionHandler aActionHandler, AnnotatorState aState, CAS aCas, - VID aSuggestionVid) + private void actionAcceptPrediction(AnnotationActionHandler aActionHandler, + AnnotatorState aState, AjaxRequestTarget aTarget, CAS aCas, VID aVID, + Optional aSuggestion, SourceDocument document) throws AnnotationException, IOException { + var suggestion = aSuggestion.get(); var page = (AnnotationPage) aTarget.getPage(); var dataOwner = aState.getUser().getUsername(); var sessionOwner = userService.getCurrentUsername(); - var layer = annotationService.getLayer(aSuggestion.getLayerId()); - var adapter = (SpanAdapter) annotationService.getAdapter(layer); + var layer = annotationService.getLayer(suggestion.getLayerId()); + var adapter = annotationService.getAdapter(layer); - var span = (Annotation) recommendationService.acceptSuggestion(sessionOwner, aSocument, - dataOwner, aCas, aSuggestion, MAIN_EDITOR); + var annotation = (Annotation) recommendationService.acceptSuggestion(sessionOwner, document, + dataOwner, aCas, suggestion, MAIN_EDITOR); page.writeEditorCas(aCas); // Set selection to the accepted annotation and select it and load it into the detail editor - aState.getSelection().set(adapter.select(VID.of(span), span)); + aState.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, aState, aSuggestionVid)); + page.send(page, BREADTH, new AjaxRecommendationAcceptedEvent(aTarget, aState, aVID)); } - private void actionAcceptRelationRecommendation(AjaxRequestTarget aTarget, - RelationSuggestion aSuggestion, SourceDocument aDocument, - AnnotationActionHandler aActionHandler, AnnotatorState aState, CAS aCas, VID aVID) - throws AnnotationException, IOException + private Optional getPrediction(AnnotatorState aState, VID aRecVid) { - var page = (AnnotationPage) aTarget.getPage(); - var dataOwner = aState.getUser().getUsername(); - var sessionOwner = userService.getCurrentUsername(); - var layer = annotationService.getLayer(aSuggestion.getLayerId()); - var adapter = (RelationAdapter) annotationService.getAdapter(layer); - - var relation = (Annotation) recommendationService.acceptSuggestion(sessionOwner, aDocument, - dataOwner, aCas, aSuggestion, MAIN_EDITOR); - - page.writeEditorCas(aCas); - - // Set selection to the accepted annotation and select it and load it into the detail editor - aState.getSelection().set(adapter.select(aVID, relation)); - - // Send a UI event that the suggestion has been accepted - page.send(page, BREADTH, new AjaxRecommendationAcceptedEvent(aTarget, aState, aVID)); + Predictions predictions = recommendationService.getPredictions(aState.getUser(), + aState.getProject()); + SourceDocument document = aState.getDocument(); + Optional prediction = predictions // + .getPredictionByVID(document, aRecVid); + return prediction; } /** diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java index 7cf9274cd90..3b543a890b7 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/config/RecommenderServiceAutoConfiguration.java @@ -49,6 +49,7 @@ import de.tudarmstadt.ukp.inception.recommendation.exporter.LearningRecordExporter; import de.tudarmstadt.ukp.inception.recommendation.exporter.RecommenderExporter; import de.tudarmstadt.ukp.inception.recommendation.footer.RecommendationEventFooterItem; +import de.tudarmstadt.ukp.inception.recommendation.link.LinkSuggestionSupport; import de.tudarmstadt.ukp.inception.recommendation.log.RecommendationAcceptedEventAdapter; import de.tudarmstadt.ukp.inception.recommendation.log.RecommendationRejectedEventAdapter; import de.tudarmstadt.ukp.inception.recommendation.log.RecommenderDeletedEventAdapter; @@ -206,8 +207,7 @@ public RecommenderActionBarExtension recommenderActionBarExtension( } @Bean - public SpanSuggestionSupport spanRecommendationSupport( - RecommendationService aRecommendationService, + public SpanSuggestionSupport spanSuggestionSupport(RecommendationService aRecommendationService, LearningRecordService aLearningRecordService, ApplicationEventPublisher aApplicationEventPublisher, AnnotationSchemaService aSchemaService, FeatureSupportRegistry aFeatureSupportRegistry, @@ -219,7 +219,7 @@ public SpanSuggestionSupport spanRecommendationSupport( } @Bean - public RelationSuggestionSupport relationRecommendationSupport( + public RelationSuggestionSupport relationSuggestionSupport( RecommendationService aRecommendationService, LearningRecordService aLearningRecordService, ApplicationEventPublisher aApplicationEventPublisher, @@ -229,6 +229,16 @@ public RelationSuggestionSupport relationRecommendationSupport( aApplicationEventPublisher, aSchemaService, aFeatureSupportRegistry); } + @Bean + public LinkSuggestionSupport linkSuggestionSupport(RecommendationService aRecommendationService, + LearningRecordService aLearningRecordService, + ApplicationEventPublisher aApplicationEventPublisher, + AnnotationSchemaService aSchemaService, FeatureSupportRegistry aFeatureSupportRegistry) + { + return new LinkSuggestionSupport(aRecommendationService, aLearningRecordService, + aApplicationEventPublisher, aSchemaService, aFeatureSupportRegistry); + } + @Bean public SuggestionSupportRegistry layerRecommendtionSupportRegistry( @Lazy @Autowired(required = false) List aExtensions) diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionRenderer.java new file mode 100644 index 00000000000..99643d1289a --- /dev/null +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionRenderer.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.link; + +import java.util.Map; + +import org.apache.uima.cas.CAS; +import org.apache.uima.cas.Type; +import org.apache.uima.cas.text.AnnotationFS; +import org.apache.uima.fit.util.CasUtil; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LinkSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.relation.ArcSuggestionRenderer_ImplBase; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VArc; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; + +public class LinkSuggestionRenderer + extends ArcSuggestionRenderer_ImplBase +{ + public LinkSuggestionRenderer(RecommendationService aRecommendationService, + AnnotationSchemaService aAnnotationService, FeatureSupportRegistry aFsRegistry) + { + super(aRecommendationService, aAnnotationService, aFsRegistry); + } + + @Override + protected VArc renderArc(AnnotationLayer aLayer, LinkSuggestion suggestion, AnnotationFS source, + AnnotationFS target, Map featureAnnotation) + { + return new VArc(aLayer, suggestion.getVID(), VID.of(source), VID.of(target), + "\uD83E\uDD16 " + suggestion.getUiLabel(), featureAnnotation, COLOR); + } + + @Override + protected Type getSourceType(CAS aCas, AnnotationLayer aLayer, AnnotationFeature aFeature) + { + return CasUtil.getType(aCas, aLayer.getName()); + } + + @Override + protected Type getTargetType(CAS aCas, AnnotationLayer aLayer, AnnotationFeature aFeature) + { + return CasUtil.getType(aCas, aFeature.getType()); + } +} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionSupport.java new file mode 100644 index 00000000000..438ae50b3ba --- /dev/null +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionSupport.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.link; + +import static org.apache.uima.fit.util.CasUtil.select; + +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.uima.cas.CAS; +import org.apache.uima.cas.Type; +import org.apache.uima.cas.text.AnnotationFS; +import org.apache.uima.fit.util.CasUtil; +import org.apache.uima.jcas.tcas.Annotation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.LinkMode; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; +import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionRenderer; +import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LinkPosition; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LinkSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Position; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.ExtractionContext; +import de.tudarmstadt.ukp.inception.recommendation.relation.ArcSuggestionSupport_ImplBase; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationComparisonUtils; +import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; +import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; +import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; +import de.tudarmstadt.ukp.inception.schema.api.feature.LinkWithRoleModel; +import de.tudarmstadt.ukp.inception.support.uima.ICasUtil; + +public class LinkSuggestionSupport + extends ArcSuggestionSupport_ImplBase +{ + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public static final String TYPE = "LINK"; + + private final FeatureSupportRegistry featureSupportRegistry; + protected final AnnotationSchemaService schemaService; + + public LinkSuggestionSupport(RecommendationService aRecommendationService, + LearningRecordService aLearningRecordService, + ApplicationEventPublisher aApplicationEventPublisher, + AnnotationSchemaService aSchemaService, FeatureSupportRegistry aFeatureSupportRegistry) + { + super(aRecommendationService, aLearningRecordService, aApplicationEventPublisher, + aSchemaService); + + schemaService = aSchemaService; + featureSupportRegistry = aFeatureSupportRegistry; + } + + @Override + public String getType() + { + return TYPE; + } + + @Override + public boolean accepts(Recommender aContext) + { + if (!SpanLayerSupport.TYPE.equals(aContext.getLayer().getType())) { + return false; + } + + var feature = aContext.getFeature(); + if (feature.getLinkMode() == LinkMode.WITH_ROLE) { + return true; + } + + return false; + } + + @Override + public AnnotationFS acceptSuggestion(String aSessionOwner, SourceDocument aDocument, + String aDataOwner, CAS aCas, TypeAdapter aAdapter, AnnotationFeature aFeature, + AnnotationSuggestion aSuggestion, LearningRecordChangeLocation aLocation, + LearningRecordUserAction aAction) + throws AnnotationException + { + var suggestion = (LinkSuggestion) aSuggestion; + var adapter = (SpanAdapter) aAdapter; + + var sourceBegin = suggestion.getPosition().getSourceBegin(); + var sourceEnd = suggestion.getPosition().getSourceEnd(); + var linkHostType = adapter.getAnnotationType(aCas); + + // Check if there is already a link host + var linkHostCandidates = aCas. select(linkHostType).at(sourceBegin, sourceEnd) + .limit(2).toList(); + Annotation linkHost = null; + if (linkHostCandidates.size() > 1) { + LOG.warn("Found multiple link host candidates, using first one..."); + linkHost = linkHostCandidates.get(0); + } + else if (!linkHostCandidates.isEmpty()) { + linkHost = linkHostCandidates.get(0); + } + + // Check if there are valid slot fillers + var targetBegin = suggestion.getPosition().getTargetBegin(); + var targetEnd = suggestion.getPosition().getTargetEnd(); + var slotFillerType = CasUtil.getType(aCas, aFeature.getType()); + var slotFillerCandidates = aCas. select(slotFillerType) + .at(targetBegin, targetEnd).limit(2).toList(); + Annotation slotFiller = null; + if (slotFillerCandidates.size() > 1) { + LOG.warn("Found multiple slot filler candidates, using first one..."); + slotFiller = slotFillerCandidates.get(0); + } + else if (!slotFillerCandidates.isEmpty()) { + slotFiller = slotFillerCandidates.get(0); + } + + try (var eventBatch = adapter.batchEvents()) { + if (linkHost == null || slotFiller == null) { + String msg = "Cannot find link host or slot filler to establish link between"; + LOG.error(msg); + throw new IllegalStateException(msg); + } + + var annotationCreated = false; + + var oldLinks = (List) adapter.getFeatureValue(aFeature, linkHost); + try { + var newLinks = new ArrayList<>(oldLinks); + var link = new LinkWithRoleModel(suggestion.getLabel(), suggestion.getLabel(), + slotFiller.getAddress()); + newLinks.add(link); + adapter.setFeatureValue(aDocument, aDataOwner, aCas, linkHost.getAddress(), + aFeature, newLinks); + + annotationCreated = true; + } + catch (Exception e) { + if (annotationCreated) { + adapter.setFeatureValue(aDocument, aDataOwner, aCas, linkHost.getAddress(), + aFeature, oldLinks); + } + throw e; + } + + hideSuggestion(aSuggestion, aAction); + recordAndPublishAcceptance(aSessionOwner, aDocument, aDataOwner, aAdapter, aFeature, + aSuggestion, linkHost, aLocation, aAction); + + eventBatch.commit(); + return linkHost; + } + } + + @Override + protected MultiValuedMap groupAnnotationsInWindow(CAS aCas, + TypeAdapter aAdapter, int aWindowBegin, int aWindowEnd) + { + var adapter = (SpanAdapter) aAdapter; + + var type = adapter.getAnnotationType(aCas); + + var annotationsInWindow = getAnnotationsInWindow(aCas, type, aWindowBegin, aWindowEnd); + + var linkFeatures = adapter.listFeatures().stream() // + .filter(f -> f.getLinkMode() == LinkMode.WITH_ROLE) // + .toList(); + + var groupedAnnotations = new ArrayListValuedHashMap(); + for (var source : annotationsInWindow) { + for (var linkFeature : linkFeatures) { + var links = (List) adapter.getFeatureValue(linkFeature, source); + + for (var link : links) { + var slotFiller = ICasUtil.selectAnnotationByAddr(aCas, link.targetAddr); + var linkPosition = new LinkPosition(linkFeature.getName(), source.getBegin(), + source.getEnd(), slotFiller.getBegin(), slotFiller.getEnd()); + + groupedAnnotations.put(linkPosition, source); + } + } + } + + return groupedAnnotations; + } + + @Override + public Optional getRenderer() + { + return Optional.of(new LinkSuggestionRenderer(recommendationService, schemaService, + featureSupportRegistry)); + } + + @Override + public List extractSuggestions(ExtractionContext ctx) + { + var adapter = schemaService.getAdapter(ctx.getLayer()); + + var result = new ArrayList(); + for (var predictedFS : ctx.getPredictionCas().select(ctx.getPredictedType())) { + if (!predictedFS.getBooleanValue(ctx.getPredictionFeature())) { + continue; + } + + var feature = ctx.getRecommender().getFeature(); + var links = (List) adapter.getFeatureValue(feature, predictedFS); + if (links.isEmpty()) { + continue; + } + + var source = (AnnotationFS) predictedFS; + var link = links.get(0); + var target = ICasUtil.selectAnnotationByAddr(ctx.getPredictionCas(), link.targetAddr); + + var originalSource = findEquivalentSpan(ctx.getOriginalCas(), source); + var originalTarget = findEquivalentSpan(ctx.getOriginalCas(), target); + if (originalSource.isEmpty() || originalTarget.isEmpty()) { + LOG.debug("Unable to find owner or slot filler of predicted link in original CAS"); + continue; + } + + var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); + var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); + var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); + var position = new LinkPosition(feature.getName(), originalSource.get(), + originalTarget.get()); + + var suggestion = LinkSuggestion.builder() // + .withId(LinkSuggestion.NEW_ID) // + .withGeneration(ctx.getGeneration()) // + .withRecommender(ctx.getRecommender()) // + .withDocument(ctx.getDocument()) // + .withPosition(position) // + .withLabel(link.role) // + .withUiLabel(link.role) // + .withScore(score) // + .withScoreExplanation(scoreExplanation) // + .withAutoAcceptMode(autoAcceptMode) // + .build(); + result.add(suggestion); + } + return result; + } + + /** + * Locates an annotation in the given CAS which is equivalent of the provided annotation. + * + * @param aOriginalCas + * the original CAS. + * @param aAnnotation + * an annotation in the prediction CAS. return the equivalent in the original CAS. + */ + private static Optional findEquivalentSpan(CAS aOriginalCas, + AnnotationFS aAnnotation) + { + return aOriginalCas. select(aAnnotation.getType()) // + .at(aAnnotation) // + .filter(candidate -> AnnotationComparisonUtils.isEquivalentSpanAnnotation(candidate, + aAnnotation, null)) + .findFirst(); + } + + private List getAnnotationsInWindow(CAS aCas, Type type, int aWindowBegin, + int aWindowEnd) + { + return select(aCas, type).stream() // + .filter(fs -> fs.coveredBy(aWindowBegin, aWindowEnd)) // + .toList(); + } +} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/ArcSuggestionRenderer_ImplBase.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/ArcSuggestionRenderer_ImplBase.java new file mode 100644 index 00000000000..177e7f643b5 --- /dev/null +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/ArcSuggestionRenderer_ImplBase.java @@ -0,0 +1,153 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.relation; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static org.apache.uima.fit.util.CasUtil.selectAt; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.uima.cas.CAS; +import org.apache.uima.cas.Type; +import org.apache.uima.cas.text.AnnotationFS; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionRenderer; +import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.ArcSuggestion_ImplBase; +import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; +import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VArc; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupport; +import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; + +public abstract class ArcSuggestionRenderer_ImplBase> + implements SuggestionRenderer +{ + private final RecommendationService recommendationService; + private final AnnotationSchemaService annotationService; + private final FeatureSupportRegistry fsRegistry; + + public ArcSuggestionRenderer_ImplBase(RecommendationService aRecommendationService, + AnnotationSchemaService aAnnotationService, FeatureSupportRegistry aFsRegistry) + { + recommendationService = aRecommendationService; + annotationService = aAnnotationService; + fsRegistry = aFsRegistry; + } + + @Override + public void render(VDocument aVDoc, RenderRequest aRequest, + SuggestionDocumentGroup aSuggestions, + AnnotationLayer aLayer) + { + var cas = aRequest.getCas(); + + // TODO #176 use the document Id once it it available in the CAS + var groupedPredictions = (SuggestionDocumentGroup) aSuggestions; + + // No recommendations to render for this layer + if (groupedPredictions.isEmpty()) { + return; + } + + recommendationService.calculateSuggestionVisibility( + aRequest.getSessionOwner().getUsername(), aRequest.getSourceDocument(), cas, + aRequest.getAnnotationUser().getUsername(), aLayer, groupedPredictions, + aRequest.getWindowBeginOffset(), aRequest.getWindowEndOffset()); + + var pref = recommendationService.getPreferences(aRequest.getAnnotationUser(), + aLayer.getProject()); + + // Bulk-load all the features of this layer to avoid having to do repeated DB accesses later + var features = annotationService.listSupportedFeatures(aLayer).stream() + .collect(toMap(AnnotationFeature::getName, identity())); + + var rankerCache = new HashMap(); + + for (var group : groupedPredictions) { + for (var suggestion : group.bestSuggestions(pref)) { + // Skip rendering AnnotationObjects that should not be rendered + if (!pref.isShowAllPredictions() && !suggestion.isVisible()) { + continue; + } + + var position = suggestion.getPosition(); + int sourceBegin = position.getSourceBegin(); + int sourceEnd = position.getSourceEnd(); + int targetBegin = position.getTargetBegin(); + int targetEnd = position.getTargetEnd(); + + // Retrieve the UI display label for the given feature value + var feature = features.get(suggestion.getFeature()); + var sourceType = getSourceType(cas, aLayer, feature); + var targetType = getTargetType(cas, aLayer, feature); + + // FIXME: We get the first match for the (begin, end) span. With stacking, there can + // be more than one and we need to get the right one then which does not need to be + // the first. We wait for #2135 for a maybe fix. + var source = selectAt(cas, sourceType, sourceBegin, sourceEnd) // + .stream().findFirst().orElse(null); + + var target = selectAt(cas, targetType, targetBegin, targetEnd) // + .stream().findFirst().orElse(null); + + if (source == null || target == null) { + continue; + } + + FeatureSupport featureSupport = fsRegistry.findExtension(feature).orElseThrow(); + var annotation = featureSupport.renderFeatureValue(feature, suggestion.getLabel()); + + Map featureAnnotation = annotation != null + ? Map.of(suggestion.getFeature(), annotation) + : Map.of(); + + var isRanker = rankerCache.computeIfAbsent(suggestion.getRecommenderId(), id -> { + var recommender = recommendationService.getRecommender(id); + if (recommender != null) { + var factory = recommendationService.getRecommenderFactory(recommender); + return factory.map(f -> f.isRanker(recommender)).orElse(false); + } + return false; + }); + + var arc = renderArc(aLayer, suggestion, source, target, featureAnnotation); + arc.setScore(suggestion.getScore()); + arc.setHideScore(isRanker); + + aVDoc.add(arc); + } + } + } + + protected abstract VArc renderArc(AnnotationLayer aLayer, T suggestion, AnnotationFS source, + AnnotationFS target, Map featureAnnotation); + + protected abstract Type getSourceType(CAS aCas, AnnotationLayer aLayer, + AnnotationFeature aFeature); + + protected abstract Type getTargetType(CAS aCas, AnnotationLayer aLayer, + AnnotationFeature aFeature); +} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/ArcSuggestionSupport_ImplBase.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/ArcSuggestionSupport_ImplBase.java new file mode 100644 index 00000000000..80b3184261a --- /dev/null +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/ArcSuggestionSupport_ImplBase.java @@ -0,0 +1,189 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.relation; + +import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_OVERLAP; + +import java.lang.invoke.MethodHandles; +import java.util.Collection; + +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.uima.cas.CAS; +import org.apache.uima.cas.Type; +import org.apache.uima.cas.text.AnnotationFS; +import org.apache.uima.fit.util.CasUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.lang.Nullable; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport_ImplBase; +import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.ArcSuggestion_ImplBase; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecord; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Position; +import de.tudarmstadt.ukp.inception.recommendation.api.model.RelationPosition; +import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; + +public abstract class ArcSuggestionSupport_ImplBase + extends SuggestionSupport_ImplBase +{ + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public ArcSuggestionSupport_ImplBase(RecommendationService aRecommendationService, + LearningRecordService aLearningRecordService, + ApplicationEventPublisher aApplicationEventPublisher, + AnnotationSchemaService aSchemaService) + { + super(aRecommendationService, aLearningRecordService, aApplicationEventPublisher, + aSchemaService); + } + + @Nullable + protected Type getAnnotationType(CAS aCas, AnnotationLayer aLayer) + { + // NOTE: In order to avoid having to upgrade the "original CAS" in computePredictions,this + // method is implemented in such a way that it gracefully handles cases where the CAS and + // the project type system are not in sync - specifically the CAS where the project defines + // layers or features which do not exist in the CAS. + + try { + return CasUtil.getAnnotationType(aCas, aLayer.getName()); + } + catch (IllegalArgumentException e) { + // Type does not exist in the type system of the CAS. Probably it has not been upgraded + // to the latest version of the type system yet. If this is the case, we'll just skip. + return null; + } + } + + public abstract String getType(); + + @Override + public void calculateSuggestionVisibility(String aSessionOwner, + SourceDocument aDocument, CAS aCas, String aUser, AnnotationLayer aLayer, + Collection> aRecommendations, int aWindowBegin, int aWindowEnd) + { + var type = getAnnotationType(aCas, aLayer); + + if (type == null) { + // The type does not exist in the type system of the CAS. Probably it has not + // been upgraded to the latest version of the type system yet. If this is the case, + // we'll just skip. + return; + } + + var adapter = schemaService.getAdapter(aLayer); + + // Group annotations by relation position, that is (source, target) address + var groupedAnnotations = groupAnnotationsInWindow(aCas, adapter, aWindowBegin, aWindowEnd); + + // Collect all suggestions of the given layer + var groupedSuggestions = aRecommendations.stream() + .filter(group -> group.getLayerId() == aLayer.getId()) // + .map(group -> (SuggestionGroup>) group) // + .toList(); + + // Get previously rejected suggestions + var groupedRecordedAnnotations = new ArrayListValuedHashMap(); + for (var learningRecord : learningRecordService.listLearningRecords(aSessionOwner, aUser, + aLayer)) { + var relationPosition = new RelationPosition(learningRecord.getOffsetSourceBegin(), + learningRecord.getOffsetSourceEnd(), learningRecord.getOffsetTargetBegin(), + learningRecord.getOffsetTargetEnd()); + + groupedRecordedAnnotations.put(relationPosition, learningRecord); + } + + for (var feature : schemaService.listSupportedFeatures(aLayer)) { + var feat = type.getFeatureByBaseName(feature.getName()); + + if (feat == null) { + // The feature does not exist in the type system of the CAS. Probably it has not + // been upgraded to the latest version of the type system yet. If this is the case, + // we'll just skip. + return; + } + + for (var group : groupedSuggestions) { + if (!feature.getName().equals(group.getFeature())) { + continue; + } + + group.showAll(AnnotationSuggestion.FLAG_ALL); + + var position = group.getPosition(); + + // If any annotation at this position has a non-null label for this feature, + // then we hide the suggestion group + for (var annotationFS : groupedAnnotations.get(position)) { + if (annotationFS.getFeatureValueAsString(feat) != null) { + for (var suggestion : group) { + suggestion.hide(FLAG_OVERLAP); + } + } + } + + // Hide previously rejected suggestions + for (var learningRecord : groupedRecordedAnnotations.get(position)) { + for (var suggestion : group) { + if (suggestion.labelEquals(learningRecord.getAnnotation())) { + suggestion.hideSuggestion(learningRecord.getUserAction()); + } + } + } + } + } + } + + protected abstract MultiValuedMap groupAnnotationsInWindow(CAS aCas, + TypeAdapter aAdapter, int aWindowBegin, int aWindowEnd); + + @Override + public LearningRecord toLearningRecord(SourceDocument aDocument, String aDataOwner, + AnnotationSuggestion aSuggestion, AnnotationFeature aFeature, + LearningRecordUserAction aUserAction, LearningRecordChangeLocation aLocation) + { + var pos = ((ArcSuggestion_ImplBase) aSuggestion).getPosition(); + var record = new LearningRecord(); + record.setUser(aDataOwner); + record.setSourceDocument(aDocument); + record.setUserAction(aUserAction); + record.setOffsetBegin(pos.getSourceBegin()); + record.setOffsetEnd(pos.getSourceEnd()); + record.setOffsetBegin2(pos.getTargetBegin()); + record.setOffsetEnd2(pos.getTargetEnd()); + record.setTokenText(""); + record.setAnnotation(aSuggestion.getLabel()); + record.setLayer(aFeature.getLayer()); + record.setSuggestionType(getType()); + record.setChangeLocation(aLocation); + record.setAnnotationFeature(aFeature); + return record; + } +} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionRenderer.java index 9ac92151a12..d017bfb5c2d 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionRenderer.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionRenderer.java @@ -17,124 +17,48 @@ */ package de.tudarmstadt.ukp.inception.recommendation.relation; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.toMap; -import static org.apache.uima.fit.util.CasUtil.selectAt; - -import java.util.HashMap; import java.util.Map; +import org.apache.uima.cas.CAS; +import org.apache.uima.cas.Type; +import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.fit.util.CasUtil; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; -import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionRenderer; -import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.RelationSuggestion; -import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; -import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; import de.tudarmstadt.ukp.inception.rendering.vmodel.VArc; -import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; -import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupport; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; public class RelationSuggestionRenderer - implements SuggestionRenderer + extends ArcSuggestionRenderer_ImplBase { - private final RecommendationService recommendationService; - private final AnnotationSchemaService annotationService; - private final FeatureSupportRegistry fsRegistry; - public RelationSuggestionRenderer(RecommendationService aRecommendationService, AnnotationSchemaService aAnnotationService, FeatureSupportRegistry aFsRegistry) { - recommendationService = aRecommendationService; - annotationService = aAnnotationService; - fsRegistry = aFsRegistry; + super(aRecommendationService, aAnnotationService, aFsRegistry); } @Override - public void render(VDocument aVDoc, RenderRequest aRequest, - SuggestionDocumentGroup aSuggestions, - AnnotationLayer aLayer) + protected VArc renderArc(AnnotationLayer aLayer, RelationSuggestion suggestion, + AnnotationFS source, AnnotationFS target, Map featureAnnotation) { - var cas = aRequest.getCas(); - - // TODO #176 use the document Id once it it available in the CAS - var groupedPredictions = (SuggestionDocumentGroup) aSuggestions; - - // No recommendations to render for this layer - if (groupedPredictions.isEmpty()) { - return; - } - - recommendationService.calculateSuggestionVisibility( - aRequest.getSessionOwner().getUsername(), aRequest.getSourceDocument(), cas, - aRequest.getAnnotationUser().getUsername(), aLayer, groupedPredictions, - aRequest.getWindowBeginOffset(), aRequest.getWindowEndOffset()); - - var pref = recommendationService.getPreferences(aRequest.getAnnotationUser(), - aLayer.getProject()); - - var attachType = CasUtil.getType(cas, aLayer.getAttachType().getName()); - - // Bulk-load all the features of this layer to avoid having to do repeated DB accesses later - var features = annotationService.listSupportedFeatures(aLayer).stream() - .collect(toMap(AnnotationFeature::getName, identity())); - - var rankerCache = new HashMap(); - - for (var group : groupedPredictions) { - for (var suggestion : group.bestSuggestions(pref)) { - // Skip rendering AnnotationObjects that should not be rendered - if (!pref.isShowAllPredictions() && !suggestion.isVisible()) { - continue; - } - - var position = suggestion.getPosition(); - int sourceBegin = position.getSourceBegin(); - int sourceEnd = position.getSourceEnd(); - int targetBegin = position.getTargetBegin(); - int targetEnd = position.getTargetEnd(); - - // FIXME: We get the first match for the (begin, end) span. With stacking, there can - // be more than one and we need to get the right one then which does not need to be - // the first. We wait for #2135 for a maybe fix. - var source = selectAt(cas, attachType, sourceBegin, sourceEnd) // - .stream().findFirst().orElse(null); - - var target = selectAt(cas, attachType, targetBegin, targetEnd) // - .stream().findFirst().orElse(null); - - // Retrieve the UI display label for the given feature value - var feature = features.get(suggestion.getFeature()); - - FeatureSupport featureSupport = fsRegistry.findExtension(feature).orElseThrow(); - var annotation = featureSupport.renderFeatureValue(feature, suggestion.getLabel()); - - Map featureAnnotation = annotation != null - ? Map.of(suggestion.getFeature(), annotation) - : Map.of(); - - var isRanker = rankerCache.computeIfAbsent(suggestion.getRecommenderId(), id -> { - var recommender = recommendationService.getRecommender(id); - if (recommender != null) { - var factory = recommendationService.getRecommenderFactory(recommender); - return factory.map(f -> f.isRanker(recommender)).orElse(false); - } - return false; - }); + return new VArc(aLayer, suggestion.getVID(), VID.of(source), VID.of(target), + "\uD83E\uDD16 " + suggestion.getUiLabel(), featureAnnotation, COLOR); + } - var arc = new VArc(aLayer, suggestion.getVID(), VID.of(source), VID.of(target), - "\uD83E\uDD16 " + suggestion.getUiLabel(), featureAnnotation, COLOR); - arc.setScore(suggestion.getScore()); - arc.setHideScore(isRanker); + @Override + protected Type getSourceType(CAS aCas, AnnotationLayer aLayer, AnnotationFeature aFeature) + { + return CasUtil.getType(aCas, aLayer.getAttachType().getName()); + } - aVDoc.add(arc); - } - } + @Override + protected Type getTargetType(CAS aCas, AnnotationLayer aLayer, AnnotationFeature aFeature) + { + return CasUtil.getType(aCas, aLayer.getAttachType().getName()); } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionSupport.java index d3595ac6c25..5451fefeade 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionSupport.java @@ -17,7 +17,6 @@ */ package de.tudarmstadt.ukp.inception.recommendation.relation; -import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_OVERLAP; import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_SOURCE; import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_TARGET; import static org.apache.uima.cas.text.AnnotationPredicates.colocated; @@ -26,11 +25,11 @@ import java.lang.invoke.MethodHandles; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Optional; +import org.apache.commons.collections4.MultiMapUtils; +import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.uima.cas.CAS; import org.apache.uima.cas.Type; @@ -40,26 +39,21 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.lang.Nullable; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationAdapter; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionRenderer; -import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport_ImplBase; 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.LearningRecordChangeLocation; import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction; import de.tudarmstadt.ukp.inception.recommendation.api.model.Position; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.model.RelationPosition; import de.tudarmstadt.ukp.inception.recommendation.api.model.RelationSuggestion; -import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.ExtractionContext; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @@ -69,7 +63,7 @@ import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; public class RelationSuggestionSupport - extends SuggestionSupport_ImplBase + extends ArcSuggestionSupport_ImplBase { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -84,9 +78,16 @@ public RelationSuggestionSupport(RecommendationService aRecommendationService, { super(aRecommendationService, aLearningRecordService, aApplicationEventPublisher, aSchemaService); + featureSupportRegistry = aFeatureSupportRegistry; } + @Override + public String getType() + { + return TYPE; + } + @Override public boolean accepts(Recommender aContext) { @@ -222,30 +223,21 @@ else if (candidates.size() > 1) { } @Override - public void calculateSuggestionVisibility(String aSessionOwner, - SourceDocument aDocument, CAS aCas, String aUser, AnnotationLayer aLayer, - Collection> aRecommendations, int aWindowBegin, int aWindowEnd) + protected MultiValuedMap groupAnnotationsInWindow(CAS aCas, + TypeAdapter aAdapter, int aWindowBegin, int aWindowEnd) { - var type = getAnnotationType(aCas, aLayer); - - if (type == null) { - // The type does not exist in the type system of the CAS. Probably it has not - // been upgraded to the latest version of the type system yet. If this is the case, - // we'll just skip. - return; - } + var adapter = (RelationAdapter) aAdapter; - var governorFeature = type.getFeatureByBaseName(FEAT_REL_SOURCE); - var dependentFeature = type.getFeatureByBaseName(FEAT_REL_TARGET); + var type = adapter.getAnnotationType(aCas); + var governorFeature = adapter.getSourceFeature(aCas); + var dependentFeature = adapter.getTargetFeature(aCas); if (dependentFeature == null || governorFeature == null) { - LOG.warn("Missing Dependent or Governor feature on [{}]", aLayer.getName()); - return; + LOG.warn("Missing Dependent or Governor feature on [{}]", type.getName()); + return MultiMapUtils.emptyMultiValuedMap(); } var annotationsInWindow = getAnnotationsInWindow(aCas, type, aWindowBegin, aWindowEnd); - - // Group annotations by relation position, that is (source, target) address var groupedAnnotations = new ArrayListValuedHashMap(); for (var annotationFS : annotationsInWindow) { var source = (AnnotationFS) annotationFS.getFeatureValue(governorFeature); @@ -257,115 +249,7 @@ public void calculateSuggestionVisibility(Strin groupedAnnotations.put(relationPosition, annotationFS); } - // Collect all suggestions of the given layer - var groupedSuggestions = aRecommendations.stream() - .filter(group -> group.getLayerId() == aLayer.getId()) // - .map(group -> (SuggestionGroup) group) // - .toList(); - - // Get previously rejected suggestions - var groupedRecordedAnnotations = new ArrayListValuedHashMap(); - for (var learningRecord : learningRecordService.listLearningRecords(aSessionOwner, aUser, - aLayer)) { - var relationPosition = new RelationPosition(learningRecord.getOffsetSourceBegin(), - learningRecord.getOffsetSourceEnd(), learningRecord.getOffsetTargetBegin(), - learningRecord.getOffsetTargetEnd()); - - groupedRecordedAnnotations.put(relationPosition, learningRecord); - } - - for (var feature : schemaService.listSupportedFeatures(aLayer)) { - var feat = type.getFeatureByBaseName(feature.getName()); - - if (feat == null) { - // The feature does not exist in the type system of the CAS. Probably it has not - // been upgraded to the latest version of the type system yet. If this is the case, - // we'll just skip. - return; - } - - for (var group : groupedSuggestions) { - if (!feature.getName().equals(group.getFeature())) { - continue; - } - - group.showAll(AnnotationSuggestion.FLAG_ALL); - - var position = group.getPosition(); - - // If any annotation at this position has a non-null label for this feature, - // then we hide the suggestion group - for (var annotationFS : groupedAnnotations.get(position)) { - if (annotationFS.getFeatureValueAsString(feat) != null) { - for (RelationSuggestion suggestion : group) { - suggestion.hide(FLAG_OVERLAP); - } - } - } - - // Hide previously rejected suggestions - for (var learningRecord : groupedRecordedAnnotations.get(position)) { - for (var suggestion : group) { - if (suggestion.labelEquals(learningRecord.getAnnotation())) { - suggestion.hideSuggestion(learningRecord.getUserAction()); - } - } - } - } - } - } - - @Nullable - private Type getAnnotationType(CAS aCas, AnnotationLayer aLayer) - { - // NOTE: In order to avoid having to upgrade the "original CAS" in computePredictions,this - // method is implemented in such a way that it gracefully handles cases where the CAS and - // the project type system are not in sync - specifically the CAS where the project defines - // layers or features which do not exist in the CAS. - - try { - return CasUtil.getAnnotationType(aCas, aLayer.getName()); - } - catch (IllegalArgumentException e) { - // Type does not exist in the type system of the CAS. Probably it has not been upgraded - // to the latest version of the type system yet. If this is the case, we'll just skip. - return null; - } - } - - private List getAnnotationsInWindow(CAS aCas, Type type, int aWindowBegin, - int aWindowEnd) - { - if (type == null) { - return Collections.emptyList(); - } - - return select(aCas, type).stream() // - .filter(fs -> fs.coveredBy(aWindowBegin, aWindowEnd)) // - .toList(); - } - - @Override - public LearningRecord toLearningRecord(SourceDocument aDocument, String aDataOwner, - AnnotationSuggestion aSuggestion, AnnotationFeature aFeature, - LearningRecordUserAction aUserAction, LearningRecordChangeLocation aLocation) - { - var pos = ((RelationSuggestion) aSuggestion).getPosition(); - var record = new LearningRecord(); - record.setUser(aDataOwner); - record.setSourceDocument(aDocument); - record.setUserAction(aUserAction); - record.setOffsetBegin(pos.getSourceBegin()); - record.setOffsetEnd(pos.getSourceEnd()); - record.setOffsetBegin2(pos.getTargetBegin()); - record.setOffsetEnd2(pos.getTargetEnd()); - record.setTokenText(""); - record.setAnnotation(aSuggestion.getLabel()); - record.setLayer(aFeature.getLayer()); - record.setSuggestionType(TYPE); - record.setChangeLocation(aLocation); - record.setAnnotationFeature(aFeature); - return record; + return groupedAnnotations; } @Override @@ -441,4 +325,12 @@ private static Optional findEquivalentSpan(CAS aOriginalCas, aAnnotation, null)) .findFirst(); } + + private List getAnnotationsInWindow(CAS aCas, Type type, int aWindowBegin, + int aWindowEnd) + { + return select(aCas, type).stream() // + .filter(fs -> fs.coveredBy(aWindowBegin, aWindowEnd)) // + .toList(); + } } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionExtractionTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionExtractionTest.java new file mode 100644 index 00000000000..556c6609b09 --- /dev/null +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionExtractionTest.java @@ -0,0 +1,199 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.link; + +import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; +import static de.tudarmstadt.ukp.inception.support.uima.AnnotationBuilder.buildAnnotation; +import static java.util.Arrays.asList; +import static org.apache.uima.util.CasCreationUtils.mergeTypeSystems; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.uima.cas.CAS; +import org.apache.uima.fit.factory.CasFactory; +import org.apache.uima.fit.factory.TypeSystemDescriptionFactory; +import org.apache.uima.fit.testing.factory.TokenBuilder; +import org.apache.uima.jcas.tcas.Annotation; +import org.apache.uima.resource.ResourceInitializationException; +import org.apache.uima.resource.metadata.impl.TypeSystemDescription_impl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +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.dkpro.core.api.segmentation.type.Sentence; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; +import de.tudarmstadt.ukp.inception.annotation.feature.link.LinkFeatureSupport; +import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerBehaviorRegistry; +import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; +import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderTypeSystemUtils; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LinkSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.ExtractionContext; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderProperties; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.schema.api.feature.LinkWithRoleModel; +import de.tudarmstadt.ukp.inception.schema.service.FeatureSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.support.uima.SegmentationUtils; + +@ExtendWith(MockitoExtension.class) +class LinkSuggestionExtractionTest +{ + private @Mock RecommendationService recommendationService; + private @Mock LearningRecordService learningRecordService; + private @Mock ApplicationEventPublisher applicationEventPublisher; + private @Mock AnnotationSchemaService schemaService; + private @Mock RecommenderProperties recommenderProperties; + private @Mock LayerBehaviorRegistry layerBehaviorRegistry; + + private TokenBuilder tokenBuilder; + private Project project; + private SourceDocument document; + private CAS originalCas; + + private LayerSupportRegistryImpl layerSupportRegistry; + private FeatureSupportRegistryImpl featureSupportRegistry; + + private LinkSuggestionSupport sut; + private LinkFeatureSupport linkFeatureSupport; + + private AnnotationLayer linkHostLayer; + private AnnotationLayer slotFillerLayer; + private AnnotationFeature linkFeature; + + private Recommender recommender; + + @BeforeEach + void setup() throws Exception + { + linkFeatureSupport = new LinkFeatureSupport(schemaService); + featureSupportRegistry = new FeatureSupportRegistryImpl(asList(linkFeatureSupport)); + featureSupportRegistry.init(); + layerSupportRegistry = new LayerSupportRegistryImpl( + asList(new SpanLayerSupport(featureSupportRegistry, null, layerBehaviorRegistry))); + layerSupportRegistry.init(); + + tokenBuilder = new TokenBuilder<>(Token.class, Sentence.class); + + project = Project.builder() // + .withId(1l) // + .withName("Test") // + .build(); + document = SourceDocument.builder() // + .withId(1l) // + .withProject(project) // + .withName("Doc") // + .build(); + linkHostLayer = AnnotationLayer.builder() // + .withId(1l) // + .withName("custom.Span") // + .withType(SpanLayerSupport.TYPE) // + .build(); + slotFillerLayer = AnnotationLayer.builder() // + .withId(1l) // + .withName("custom.Arg") // + .withType(SpanLayerSupport.TYPE) // + .build(); + linkFeature = AnnotationFeature.builder() // + .withLayer(linkHostLayer) // + .withType(slotFillerLayer.getName()) // + .withName("args") // + .build(); + linkFeatureSupport.configureFeature(linkFeature); + + recommender = Recommender.builder() // + .withId(1l) // + .withName("recommender") // + .withProject(project) // + .withLayer(linkHostLayer) // + .withFeature(linkFeature) // + .build(); + + originalCas = createCas(asList(linkHostLayer, slotFillerLayer), asList(linkFeature)); + originalCas.setDocumentText("This is a test."); + + SegmentationUtils.splitSentences(originalCas); + SegmentationUtils.tokenize(originalCas); + + sut = new LinkSuggestionSupport(recommendationService, learningRecordService, + applicationEventPublisher, schemaService, featureSupportRegistry); + + var linkHostLayerAdapter = layerSupportRegistry.getLayerSupport(linkHostLayer) + .createAdapter(linkHostLayer, () -> asList(linkFeature)); + when(schemaService.getAdapter(linkHostLayer)).thenReturn(linkHostLayerAdapter); + } + + @Test + void testLinkExtraction() throws Exception + { + var slotFiller = buildAnnotation(originalCas, slotFillerLayer.getName()) // + .onMatch("\\btest\\b") // + .buildAndAddToIndexes(); + + var linkHost = buildAnnotation(originalCas, linkHostLayer.getName()) // + .onMatch("\\bis\\b") // + .buildAndAddToIndexes(); + + var predictionCas = RecommenderTypeSystemUtils.makePredictionCas(originalCas, linkFeature); + + var preSlotFiller = predictionCas. select(slotFillerLayer.getName()).get(); + var prediction = buildAnnotation(predictionCas, linkHostLayer.getName()) // + .onMatch("\\bis\\b") // + .withFeature(FEATURE_NAME_IS_PREDICTION, true) // + .buildAndAddToIndexes(); + linkFeatureSupport.setFeatureValue(predictionCas, linkFeature, prediction.getAddress(), + asList(new LinkWithRoleModel("role", "label", preSlotFiller.getAddress()))); + + var ctx = new ExtractionContext(0, recommender, document, originalCas, predictionCas); + var suggestions = sut.extractSuggestions(ctx); + + assertThat(suggestions) // + .filteredOn(a -> a instanceof LinkSuggestion) // + .map(a -> (LinkSuggestion) a) // + .extracting( // + LinkSuggestion::getRecommenderName, // + LinkSuggestion::getLabel) // + .containsExactly( // + tuple(recommender.getName(), "role")); + } + + private CAS createCas(List aLayers, List aFeatures) + throws ResourceInitializationException + { + var globalTypes = TypeSystemDescriptionFactory.createTypeSystemDescription(); + var localTypes = new TypeSystemDescription_impl(); + + for (var layer : aLayers) { + layerSupportRegistry.getLayerSupport(layer).generateTypes(localTypes, layer, aFeatures); + } + + return CasFactory.createCas(mergeTypeSystems(asList(globalTypes, localTypes))); + } +} diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionVisibilityCalculationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionVisibilityCalculationTest.java new file mode 100644 index 00000000000..863745ff8aa --- /dev/null +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/link/LinkSuggestionVisibilityCalculationTest.java @@ -0,0 +1,259 @@ +/* + * Licensed to the Technische Universität Darmstadt under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The Technische Universität Darmstadt + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.tudarmstadt.ukp.inception.recommendation.link; + +import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.getInvisibleSuggestions; +import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.getVisibleSuggestions; +import static de.tudarmstadt.ukp.inception.support.uima.AnnotationBuilder.buildAnnotation; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.apache.uima.util.CasCreationUtils.mergeTypeSystems; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.uima.cas.CAS; +import org.apache.uima.fit.factory.CasFactory; +import org.apache.uima.fit.factory.TypeSystemDescriptionFactory; +import org.apache.uima.resource.metadata.impl.TypeSystemDescription_impl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +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.inception.annotation.feature.link.LinkFeatureSupport; +import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerBehaviorRegistry; +import de.tudarmstadt.ukp.inception.annotation.layer.behaviors.LayerSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; +import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; +import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LinkPosition; +import de.tudarmstadt.ukp.inception.recommendation.api.model.LinkSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; +import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; +import de.tudarmstadt.ukp.inception.schema.api.feature.LinkWithRoleModel; +import de.tudarmstadt.ukp.inception.schema.service.FeatureSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.support.uima.SegmentationUtils; + +@ExtendWith(MockitoExtension.class) +public class LinkSuggestionVisibilityCalculationTest +{ + private final static String TEST_USER = "Testuser"; + private final static long RECOMMENDER_ID = 1; + private final static String RECOMMENDER_NAME = "TestEntityRecommender"; + private final static double CONFIDENCE = 0.2; + private final static String CONFIDENCE_EXPLANATION = "Predictor A: 0.05 | Predictor B: 0.15"; + + private @Mock LearningRecordService learningRecordService; + private @Mock AnnotationSchemaService schemaService; + private @Mock LayerBehaviorRegistry layerBehaviorRegistry; + + private LinkSuggestionSupport sut; + + private LayerSupportRegistryImpl layerSupportRegistry; + private FeatureSupportRegistryImpl featureSupportRegistry; + + private LinkFeatureSupport linkFeatureSupport; + + private Project project; + private SourceDocument document; + private AnnotationLayer linkHostLayer; + private AnnotationLayer slotFillerLayer; + private AnnotationFeature linkFeature; + + private CAS cas; + private Recommender recommender; + private TypeAdapter linkHostLayerAdapter; + + @BeforeEach + public void setUp() throws Exception + { + linkFeatureSupport = new LinkFeatureSupport(schemaService); + featureSupportRegistry = new FeatureSupportRegistryImpl(asList(linkFeatureSupport)); + featureSupportRegistry.init(); + layerSupportRegistry = new LayerSupportRegistryImpl( + asList(new SpanLayerSupport(featureSupportRegistry, null, layerBehaviorRegistry))); + layerSupportRegistry.init(); + + project = Project.builder() // + .withId(1l) // + .withName("Test") // + .build(); + document = SourceDocument.builder() // + .withId(1l) // + .withProject(project) // + .withName("Doc") // + .build(); + linkHostLayer = AnnotationLayer.builder() // + .withId(1l) // + .withName("custom.Span") // + .withType(SpanLayerSupport.TYPE) // + .build(); + slotFillerLayer = AnnotationLayer.builder() // + .withId(1l) // + .withName("custom.Arg") // + .withType(SpanLayerSupport.TYPE) // + .build(); + linkFeature = AnnotationFeature.builder() // + .withLayer(linkHostLayer) // + .withType(slotFillerLayer.getName()) // + .withName("args") // + .build(); + linkFeatureSupport.configureFeature(linkFeature); + recommender = Recommender.builder() // + .withId(RECOMMENDER_ID) // + .withName(RECOMMENDER_NAME) // + .withLayer(linkFeature.getLayer()) // + .withFeature(linkFeature) // + .build(); + + cas = createCas(asList(linkHostLayer, slotFillerLayer), asList(linkFeature)); + cas.setDocumentText("This is a test."); + + SegmentationUtils.splitSentences(cas); + SegmentationUtils.tokenize(cas); + + when(schemaService.listSupportedFeatures(linkHostLayer)).thenReturn(asList(linkFeature)); + + sut = new LinkSuggestionSupport(null, learningRecordService, null, schemaService, null); + + linkHostLayerAdapter = layerSupportRegistry.getLayerSupport(linkHostLayer) + .createAdapter(linkHostLayer, () -> asList(linkFeature)); + when(schemaService.getAdapter(linkHostLayer)).thenReturn(linkHostLayerAdapter); + } + + @Test + public void testCalculateVisibilityNoRecordsAllHidden() throws Exception + { + doReturn(emptyList()).when(learningRecordService).listLearningRecords(TEST_USER, TEST_USER, + linkHostLayer); + + var slotFiller = buildAnnotation(cas, slotFillerLayer.getName()) // + .onMatch("\\btest\\b") // + .buildAndAddToIndexes(); + + var linkHost = buildAnnotation(cas, linkHostLayer.getName()) // + .onMatch("\\bis\\b") // + .buildAndAddToIndexes(); + linkHostLayerAdapter.setFeatureValue(document, TEST_USER, linkHost, linkFeature, + asList(new LinkWithRoleModel("role", slotFiller))); + + var suggestions = makeLinkSuggestionGroup(document, linkFeature, + new int[][] { { 1, linkHost.getBegin(), linkHost.getEnd(), slotFiller.getBegin(), + slotFiller.getEnd() } }); + + sut.calculateSuggestionVisibility(TEST_USER, document, cas, TEST_USER, linkHostLayer, + suggestions, 0, cas.getDocumentText().length()); + + assertThat(getVisibleSuggestions(suggestions)) // + .as("No suggestions are visible as they overlap with annotations") // + .isEmpty(); + + assertThat(getInvisibleSuggestions(suggestions)) // + .as("Invisible suggestions are hidden because of overlapping") // + .extracting(AnnotationSuggestion::getReasonForHiding) // + .extracting(String::trim) // + .containsExactly("overlapping"); + } + + @Test + public void thatVisibilityIsRestoredWhenOverlappingAnnotationIsRemoved() throws Exception + { + doReturn(emptyList()).when(learningRecordService).listLearningRecords(TEST_USER, TEST_USER, + linkHostLayer); + + var slotFiller = buildAnnotation(cas, slotFillerLayer.getName()) // + .onMatch("\\btest\\b") // + .buildAndAddToIndexes(); + + var linkHost = buildAnnotation(cas, linkHostLayer.getName()) // + .onMatch("\\bis\\b") // + .buildAndAddToIndexes(); + linkHostLayerAdapter.setFeatureValue(document, TEST_USER, linkHost, linkFeature, + asList(new LinkWithRoleModel("role", slotFiller))); + + var suggestions = makeLinkSuggestionGroup(document, linkFeature, + new int[][] { { 1, linkHost.getBegin(), linkHost.getEnd(), slotFiller.getBegin(), + slotFiller.getEnd() } }); + + sut.calculateSuggestionVisibility(TEST_USER, document, cas, TEST_USER, linkHostLayer, + suggestions, 0, 25); + + assertThat(getVisibleSuggestions(suggestions)) // + .as("No suggestions are visible as they overlap with annotations") // + .isEmpty(); + assertThat(getInvisibleSuggestions(suggestions)) // + .as("All suggestions are hidden as the overlap with annotations") // + .isNotEmpty(); + + linkHostLayerAdapter.setFeatureValue(document, TEST_USER, linkHost, linkFeature, asList()); + + sut.calculateSuggestionVisibility(TEST_USER, document, cas, TEST_USER, linkHostLayer, + suggestions, 0, 25); + + assertThat(getInvisibleSuggestions(suggestions)) // + .as("No suggestions are hidden as they no longer overlap with annotations") // + .containsExactly(); + assertThat(getVisibleSuggestions(suggestions)) // + .as("All suggestions are visible as they no longer overlap with annotations") // + .containsExactlyInAnyOrderElementsOf( + suggestions.stream().flatMap(g -> g.stream()).toList()); + } + + private CAS createCas(List aLayers, List aFeatures) + throws Exception + { + var globalTypes = TypeSystemDescriptionFactory.createTypeSystemDescription(); + var localTypes = new TypeSystemDescription_impl(); + + for (var layer : aLayers) { + layerSupportRegistry.getLayerSupport(layer).generateTypes(localTypes, layer, aFeatures); + } + + return CasFactory.createCas(mergeTypeSystems(asList(globalTypes, localTypes))); + } + + SuggestionDocumentGroup makeLinkSuggestionGroup(SourceDocument doc, + AnnotationFeature aFeat, int[][] vals) + { + var suggestions = new ArrayList(); + for (int[] val : vals) { + var suggestion = LinkSuggestion.builder() // + .withId(val[0]) // + .withRecommender(recommender) // + .withDocument(doc) // + .withPosition(new LinkPosition(aFeat.getName(), val[1], val[2], val[3], val[4])) // + .withScore(CONFIDENCE) // + .withScoreExplanation(CONFIDENCE_EXPLANATION) // + .build(); + suggestions.add(suggestion); + } + + return SuggestionDocumentGroup.groupsOfType(LinkSuggestion.class, suggestions); + } +} diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionVisibilityCalculationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionVisibilityCalculationTest.java index 35e431c88b4..f255794eec7 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionVisibilityCalculationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionVisibilityCalculationTest.java @@ -19,7 +19,6 @@ import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.getInvisibleSuggestions; import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.getVisibleSuggestions; -import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.makeRelationSuggestionGroup; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; @@ -28,6 +27,9 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.List; + import org.apache.uima.cas.CAS; import org.apache.uima.fit.factory.JCasFactory; import org.junit.jupiter.api.BeforeEach; @@ -44,6 +46,10 @@ import de.tudarmstadt.ukp.dkpro.core.api.syntax.type.dependency.Dependency; import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.model.RelationPosition; +import de.tudarmstadt.ukp.inception.recommendation.api.model.RelationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @ExtendWith(MockitoExtension.class) @@ -51,6 +57,11 @@ public class RelationSuggestionVisibilityCalculationTest { private static final String TEST_USER = "Testuser"; + private final static long RECOMMENDER_ID = 1; + private final static String RECOMMENDER_NAME = "TestEntityRecommender"; + private final static double CONFIDENCE = 0.2; + private final static String CONFIDENCE_EXPLANATION = "Predictor A: 0.05 | Predictor B: 0.15"; + private @Mock AnnotationSchemaService annoService; private @Mock LearningRecordService learningRecordService; @@ -154,4 +165,22 @@ private CAS getTestCas() throws Exception return jcas.getCas(); } + + static SuggestionDocumentGroup makeRelationSuggestionGroup( + SourceDocument doc, AnnotationFeature aFeat, int[][] vals) + { + var rec = Recommender.builder().withId(RECOMMENDER_ID).withName(RECOMMENDER_NAME) + .withLayer(aFeat.getLayer()).withFeature(aFeat).build(); + + List suggestions = new ArrayList<>(); + for (int[] val : vals) { + var suggestion = RelationSuggestion.builder().withId(val[0]).withRecommender(rec) + .withDocument(doc) + .withPosition(new RelationPosition(val[1], val[2], val[3], val[4])) + .withScore(CONFIDENCE).withScoreExplanation(CONFIDENCE_EXPLANATION).build(); + suggestions.add(suggestion); + } + + return SuggestionDocumentGroup.groupsOfType(RelationSuggestion.class, suggestions); + } } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/Fixtures.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/Fixtures.java index 4bc0266f1a4..bdf79b2e947 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/Fixtures.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/Fixtures.java @@ -25,8 +25,6 @@ import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; -import de.tudarmstadt.ukp.inception.recommendation.api.model.RelationPosition; -import de.tudarmstadt.ukp.inception.recommendation.api.model.RelationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; @@ -74,22 +72,4 @@ public static SuggestionDocumentGroup makeSpanSuggestionGroup( return SuggestionDocumentGroup.groupsOfType(SpanSuggestion.class, suggestions); } - - public static SuggestionDocumentGroup makeRelationSuggestionGroup( - SourceDocument doc, AnnotationFeature aFeat, int[][] vals) - { - var rec = Recommender.builder().withId(RECOMMENDER_ID).withName(RECOMMENDER_NAME) - .withLayer(aFeat.getLayer()).withFeature(aFeat).build(); - - List suggestions = new ArrayList<>(); - for (int[] val : vals) { - var suggestion = RelationSuggestion.builder().withId(val[0]).withRecommender(rec) - .withDocument(doc) - .withPosition(new RelationPosition(val[1], val[2], val[3], val[4])) - .withScore(CONFIDENCE).withScoreExplanation(CONFIDENCE_EXPLANATION).build(); - suggestions.add(suggestion); - } - - return SuggestionDocumentGroup.groupsOfType(RelationSuggestion.class, suggestions); - } } diff --git a/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/adapter/TypeAdapter.java b/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/adapter/TypeAdapter.java index 943a93b8ce6..633027e0285 100644 --- a/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/adapter/TypeAdapter.java +++ b/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/adapter/TypeAdapter.java @@ -154,6 +154,15 @@ void setFeatureValue(SourceDocument aDocument, String aDocumentOwner, CAS aCas, AnnotationFeature aFeature, Object aValue) throws AnnotationException; + default void setFeatureValue(SourceDocument aDocument, String aDocumentOwner, + FeatureStructure aFs, AnnotationFeature aFeature, Object aValue) + throws AnnotationException + + { + setFeatureValue(aDocument, aDocumentOwner, aFs.getCAS(), aFs.getAddress(), aFeature, + aValue); + } + /** * Get the value of the given feature. * diff --git a/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/feature/LinkWithRoleModel.java b/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/feature/LinkWithRoleModel.java index f0de33544ef..e0e811af39f 100644 --- a/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/feature/LinkWithRoleModel.java +++ b/inception/inception-schema-api/src/main/java/de/tudarmstadt/ukp/inception/schema/api/feature/LinkWithRoleModel.java @@ -19,6 +19,8 @@ import java.io.Serializable; +import org.apache.uima.cas.FeatureStructure; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @@ -51,6 +53,13 @@ public LinkWithRoleModel(LinkWithRoleModel aOther) targetAddr = aOther.targetAddr; } + public LinkWithRoleModel(String aRole, FeatureStructure aFS) + { + role = aRole; + label = aRole; + targetAddr = aFS.getAddress(); + } + public LinkWithRoleModel(String aRole, String aLabel, int aTargetAddr) { role = aRole; diff --git a/inception/inception-schema/src/main/java/de/tudarmstadt/ukp/inception/schema/service/AnnotationSchemaServiceImpl.java b/inception/inception-schema/src/main/java/de/tudarmstadt/ukp/inception/schema/service/AnnotationSchemaServiceImpl.java index 99e3227af45..5861b3b0872 100644 --- a/inception/inception-schema/src/main/java/de/tudarmstadt/ukp/inception/schema/service/AnnotationSchemaServiceImpl.java +++ b/inception/inception-schema/src/main/java/de/tudarmstadt/ukp/inception/schema/service/AnnotationSchemaServiceImpl.java @@ -1068,17 +1068,17 @@ public TypeSystemDescription getCustomProjectTypes(Project aProject) public TypeSystemDescription getAllProjectTypes(Project aProject) throws ResourceInitializationException { - List allLayersInProject = listSupportedLayers(aProject); - List allFeaturesInProject = listSupportedFeatures(aProject); + var allLayersInProject = listSupportedLayers(aProject); + var allFeaturesInProject = listSupportedFeatures(aProject); - List allTsds = new ArrayList<>(); - for (AnnotationLayer layer : allLayersInProject) { + var allTsds = new ArrayList(); + for (var layer : allLayersInProject) { LayerSupport layerSupport = layerSupportRegistry.getLayerSupport(layer); // for built-in layers, we clone the information from the built-in type descriptors - TypeSystemDescription tsd = new TypeSystemDescription_impl(); + var tsd = new TypeSystemDescription_impl(); if (layer.isBuiltIn()) { - for (String typeName : layerSupport.getGeneratedTypeNames(layer)) { + for (var typeName : layerSupport.getGeneratedTypeNames(layer)) { exportBuiltInTypeDescription(builtInTypes, tsd, typeName); } } @@ -1091,14 +1091,14 @@ public TypeSystemDescription getAllProjectTypes(Project aProject) { // Explicitly add Token because the layer may not be declared in the project - TypeSystemDescription tsd = new TypeSystemDescription_impl(); + var tsd = new TypeSystemDescription_impl(); exportBuiltInTypeDescription(builtInTypes, tsd, Token.class.getName()); allTsds.add(tsd); } { // Explicitly add Sentence because the layer may not be declared in the project - TypeSystemDescription tsd = new TypeSystemDescription_impl(); + var tsd = new TypeSystemDescription_impl(); exportBuiltInTypeDescription(builtInTypes, tsd, Sentence.class.getName()); allTsds.add(tsd); } @@ -1111,18 +1111,18 @@ public TypeSystemDescription getAllProjectTypes(Project aProject) private void exportBuiltInTypeDescription(TypeSystemDescription aSource, TypeSystemDescription aTarget, String aType) { - TypeDescription builtInType = aSource.getType(aType); + var builtInType = aSource.getType(aType); if (builtInType == null) { throw new IllegalArgumentException( "No type description found for type [" + aType + "]"); } - TypeDescription clonedType = aTarget.addType(builtInType.getName(), - builtInType.getDescription(), builtInType.getSupertypeName()); + var clonedType = aTarget.addType(builtInType.getName(), builtInType.getDescription(), + builtInType.getSupertypeName()); if (builtInType.getFeatures() != null) { - for (FeatureDescription feature : builtInType.getFeatures()) { + for (var feature : builtInType.getFeatures()) { clonedType.addFeature(feature.getName(), feature.getDescription(), feature.getRangeTypeName(), feature.getElementType(), feature.getMultipleReferencesAllowed());