From 7ffa93746806f3d47699357d334e7f9f0438dda8 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Mon, 18 Dec 2023 06:41:08 +0100 Subject: [PATCH 01/13] #4383 - Hide negative scores in annotation sidebar but still sort by it - Use negative numbers only for sorting but do not display them in the sidebar --- .../inception-diam-editor/src/main/ts/src/LabelBadge.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/inception/inception-diam-editor/src/main/ts/src/LabelBadge.svelte b/inception/inception-diam-editor/src/main/ts/src/LabelBadge.svelte index 0e28e795d8e..fcd4297a592 100644 --- a/inception/inception-diam-editor/src/main/ts/src/LabelBadge.svelte +++ b/inception/inception-diam-editor/src/main/ts/src/LabelBadge.svelte @@ -61,7 +61,8 @@ {#if showText} {renderLabel(annotation)} {/if} - {#if annotation.score} + + {#if annotation.score > 0} {annotation.score.toFixed(2)} @@ -88,7 +89,8 @@ {#if showText} {renderLabel(annotation)} {/if} - {#if annotation.score} + + {#if annotation.score > 0} {annotation.score.toFixed(2)} From 802b6c8ff0470f24b3a7907578023e3f2dae12f2 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Mon, 18 Dec 2023 20:35:24 +0100 Subject: [PATCH 02/13] #4383 - Hide negative scores in annotation sidebar but still sort by it - Added flag to VDoc annotations to indicate if a score should be hidden - Added a flag to the external recommender that indicates if it is a ranker - for rankers, the score should be hidden - Hide the score in the annotation sidebar if the hide flag is set --- .../compactv2/CompactAnnotationAttributes.java | 16 ++++++++++++++++ .../model/compactv2/CompactSerializerV2Impl.java | 2 ++ .../src/main/ts/src/LabelBadge.svelte | 4 ++-- .../external/v1/ExternalRecommenderFactory.java | 7 +++++++ .../external/v1/ExternalRecommenderTraits.java | 15 +++++++++++++-- .../v1/ExternalRecommenderTraitsEditor.html | 10 ++++++++++ .../v1/ExternalRecommenderTraitsEditor.java | 4 ++++ .../ExternalRecommenderTraitsEditor.properties | 1 + .../src/main/ts/src/model/Annotation.ts | 5 +++++ .../src/main/ts/src/model/Relation.ts | 2 ++ .../src/main/ts/src/model/Span.ts | 2 ++ .../compact_v2/CompactAnnotationAttributes.ts | 5 +++++ .../ts/src/model/compact_v2/CompactRelation.ts | 1 + .../main/ts/src/model/compact_v2/CompactSpan.ts | 1 + .../ukp/inception/rendering/vmodel/VObject.java | 11 +++++++++++ .../api/model/SuggestionGroup.java | 10 +++------- .../recommender/RecommendationEngineFactory.java | 12 ++++++++++++ .../RecommendationEditorExtension.java | 2 +- .../render/RecommendationRelationRenderer.java | 13 +++++++++++++ .../render/RecommendationSpanRenderer.java | 13 +++++++++++++ 20 files changed, 124 insertions(+), 12 deletions(-) diff --git a/inception/inception-diam-compactv2/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactAnnotationAttributes.java b/inception/inception-diam-compactv2/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactAnnotationAttributes.java index 22868dd035d..bb88495acbe 100644 --- a/inception/inception-diam-compactv2/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactAnnotationAttributes.java +++ b/inception/inception-diam-compactv2/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactAnnotationAttributes.java @@ -20,6 +20,8 @@ import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonFormat.Shape; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; @@ -32,11 +34,13 @@ public class CompactAnnotationAttributes public static final String ATTR_COLOR = "c"; public static final String ATTR_COMMENTS = "cm"; public static final String ATTR_SCORE = "s"; + public static final String ATTR_HIDE_SCORE = "hs"; private @JsonProperty(ATTR_LABEL) String labelText; private @JsonProperty(ATTR_COLOR) String color; private @JsonProperty(ATTR_COMMENTS) List comments; private @JsonProperty(ATTR_SCORE) double score; + private @JsonProperty(ATTR_HIDE_SCORE) boolean hideScore; @JsonInclude(Include.NON_DEFAULT) @JsonSerialize(using = ScoreSerializer.class) @@ -50,6 +54,18 @@ public void setScore(double aScore) score = aScore; } + @JsonFormat(shape = Shape.NUMBER) + @JsonInclude(Include.NON_DEFAULT) + public boolean isHideScore() + { + return hideScore; + } + + public void setHideScore(boolean aHideScore) + { + hideScore = aHideScore; + } + public void setLabelText(String aLabelText) { labelText = aLabelText; diff --git a/inception/inception-diam-compactv2/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactSerializerV2Impl.java b/inception/inception-diam-compactv2/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactSerializerV2Impl.java index 0a598e9dc4a..9955bfd9769 100644 --- a/inception/inception-diam-compactv2/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactSerializerV2Impl.java +++ b/inception/inception-diam-compactv2/src/main/java/de/tudarmstadt/ukp/inception/diam/model/compactv2/CompactSerializerV2Impl.java @@ -125,6 +125,7 @@ private CompactRelation renderRelation(VArc varc) getArgument(varc.getSource(), varc.getTarget()), varc.getLabelHint(), varc.getColorHint()); carc.getAttributes().setScore(varc.getScore()); + carc.getAttributes().setHideScore(varc.isHideScore()); return carc; } @@ -151,6 +152,7 @@ private CompactSpan renderSpan(RenderRequest aRequest, VSpan vspan) vspan.getColorHint()); } cspan.getAttributes().setScore(vspan.getScore()); + cspan.getAttributes().setHideScore(vspan.isHideScore()); return cspan; } diff --git a/inception/inception-diam-editor/src/main/ts/src/LabelBadge.svelte b/inception/inception-diam-editor/src/main/ts/src/LabelBadge.svelte index fcd4297a592..12d8158f929 100644 --- a/inception/inception-diam-editor/src/main/ts/src/LabelBadge.svelte +++ b/inception/inception-diam-editor/src/main/ts/src/LabelBadge.svelte @@ -62,7 +62,7 @@ {renderLabel(annotation)} {/if} - {#if annotation.score > 0} + {#if annotation.score && !annotation.hideScore} {annotation.score.toFixed(2)} @@ -90,7 +90,7 @@ {renderLabel(annotation)} {/if} - {#if annotation.score > 0} + {#if annotation.score && !annotation.hideScore} {annotation.score.toFixed(2)} diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderFactory.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderFactory.java index 89b392f38e2..c12a7429638 100644 --- a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderFactory.java +++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderFactory.java @@ -97,4 +97,11 @@ public boolean isEvaluable() { return false; } + + @Override + public boolean isRanker(Recommender aRecommender) + { + var traits = readTraits(aRecommender); + return traits.isRanker(); + } } diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraits.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraits.java index a9765586679..44925b48422 100644 --- a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraits.java +++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraits.java @@ -30,6 +30,7 @@ public class ExternalRecommenderTraits private String remoteUrl; private boolean trainable; private boolean verifyCertificates = true; + private boolean ranker; public String getRemoteUrl() { @@ -50,14 +51,24 @@ public void setTrainable(boolean aTrainable) { trainable = aTrainable; } - + public void setVerifyCertificates(boolean aVerifyCertificates) { verifyCertificates = aVerifyCertificates; } - + public boolean isVerifyCertificates() { return verifyCertificates; } + + public void setRanker(boolean aRanker) + { + ranker = aRanker; + } + + public boolean isRanker() + { + return ranker; + } } diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.html b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.html index 59a1a5ad665..aa10c097d97 100644 --- a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.html +++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.html @@ -47,6 +47,16 @@ +
+
+
+ + +
+
+
diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.java index e23e338253e..451c4b4ef53 100644 --- a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.java +++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.java @@ -80,6 +80,10 @@ protected void onSubmit() getTrainingStatesChoice().add(visibleWhen(() -> trainable.getModelObject() == true)); + var ranker = new CheckBox("ranker"); + ranker.setOutputMarkupId(true); + form.add(ranker); + add(form); } } diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.properties b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.properties index 4bd43e422ab..c9fedd4042a 100644 --- a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.properties +++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderTraitsEditor.properties @@ -16,4 +16,5 @@ remoteUrl=Remote URL trainable=Trainable +ranker=Ranker verifyCertificates=Verify certificates diff --git a/inception/inception-js-api/src/main/ts/src/model/Annotation.ts b/inception/inception-js-api/src/main/ts/src/model/Annotation.ts index cf14b811642..6ff6c1712f0 100644 --- a/inception/inception-js-api/src/main/ts/src/model/Annotation.ts +++ b/inception/inception-js-api/src/main/ts/src/model/Annotation.ts @@ -39,6 +39,11 @@ export interface Annotation { */ score?: number + /** + * Whether to display the score in the UI or not. + */ + hideScore: boolean + /** * Comments */ diff --git a/inception/inception-js-api/src/main/ts/src/model/Relation.ts b/inception/inception-js-api/src/main/ts/src/model/Relation.ts index faa638495ff..2c11a8d9918 100644 --- a/inception/inception-js-api/src/main/ts/src/model/Relation.ts +++ b/inception/inception-js-api/src/main/ts/src/model/Relation.ts @@ -24,6 +24,7 @@ export class Relation implements Annotation { color?: string label?: string score?: number + hideScore: boolean comments?: Comment[] arguments: Array @@ -36,6 +37,7 @@ export class Relation implements Annotation { this.label = other.label this.comments = other.comments this.arguments = other.arguments + this.hideScore = other.hideScore } } } diff --git a/inception/inception-js-api/src/main/ts/src/model/Span.ts b/inception/inception-js-api/src/main/ts/src/model/Span.ts index a530429c6f7..fe7314cb705 100644 --- a/inception/inception-js-api/src/main/ts/src/model/Span.ts +++ b/inception/inception-js-api/src/main/ts/src/model/Span.ts @@ -28,6 +28,7 @@ export class Span implements Annotation { color?: string label?: string score?: number + hideScore: boolean comments?: Comment[] /** @@ -45,6 +46,7 @@ export class Span implements Annotation { this.label = other.label this.comments = other.comments this.clippingFlags = other.clippingFlags + this.hideScore = other.hideScore } } } diff --git a/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactAnnotationAttributes.ts b/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactAnnotationAttributes.ts index fc3583a932b..370f41fad85 100644 --- a/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactAnnotationAttributes.ts +++ b/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactAnnotationAttributes.ts @@ -37,4 +37,9 @@ export interface CompactAnnotationAttributes { * Score (optional) */ s: number + + /** + * Hide score (optional) + */ + hs: number } diff --git a/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactRelation.ts b/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactRelation.ts index 5adc31dd234..ada22e9236b 100644 --- a/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactRelation.ts +++ b/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactRelation.ts @@ -36,6 +36,7 @@ export function unpackCompactRelation (doc: AnnotatedText, raw: CompactRelation) cooked.color = raw[3]?.c cooked.label = raw[3]?.l cooked.score = raw[3]?.s + cooked.hideScore = raw[3]?.hs ? true : false cooked.comments = unpackCompactComments(doc, cooked, raw[3]?.cm) return cooked } diff --git a/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactSpan.ts b/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactSpan.ts index e7e1957383f..914f0cbac47 100644 --- a/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactSpan.ts +++ b/inception/inception-js-api/src/main/ts/src/model/compact_v2/CompactSpan.ts @@ -35,6 +35,7 @@ export function unpackCompactSpan (doc: AnnotatedText, raw: CompactSpan): Span { cooked.color = raw[3]?.c cooked.label = raw[3]?.l cooked.score = raw[3]?.s + cooked.hideScore = raw[3]?.hs ? true : false cooked.clippingFlags = raw[3]?.cl cooked.comments = unpackCompactComments(doc, cooked, raw[3]?.cm) return cooked diff --git a/inception/inception-model-vdoc/src/main/java/de/tudarmstadt/ukp/inception/rendering/vmodel/VObject.java b/inception/inception-model-vdoc/src/main/java/de/tudarmstadt/ukp/inception/rendering/vmodel/VObject.java index 28320436fe4..f1543b0bf61 100644 --- a/inception/inception-model-vdoc/src/main/java/de/tudarmstadt/ukp/inception/rendering/vmodel/VObject.java +++ b/inception/inception-model-vdoc/src/main/java/de/tudarmstadt/ukp/inception/rendering/vmodel/VObject.java @@ -36,6 +36,7 @@ public abstract class VObject private String color; private String label; private double score; + private boolean hideScore; private boolean actionButtons; public VObject(AnnotationLayer aLayer, VID aVid, Map aFeatures) @@ -116,4 +117,14 @@ public void setScore(double aScore) { score = aScore; } + + public boolean isHideScore() + { + return hideScore; + } + + public void setHideScore(boolean aHideScore) + { + hideScore = aHideScore; + } } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionGroup.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionGroup.java index da7526e937c..0bb7cdcea3f 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionGroup.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionGroup.java @@ -178,13 +178,9 @@ public List bestSuggestions(Preferences aPreferences) // Determine the maximum score per Label Map maxScorePerLabel = new HashMap<>(); - for (LabelMapKey label : labelMap.keySet()) { - double maxScore = 0; - for (Entry classifier : labelMap.get(label).entrySet()) { - if (classifier.getValue().getScore() > maxScore) { - maxScore = classifier.getValue().getScore(); - } - } + for (var label : labelMap.keySet()) { + double maxScore = labelMap.get(label).values().stream() + .mapToDouble(AnnotationSuggestion::getScore).max().orElse(0.0d); maxScorePerLabel.put(label, maxScore); } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommendationEngineFactory.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommendationEngineFactory.java index 141f2b14256..498812c5e22 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommendationEngineFactory.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommendationEngineFactory.java @@ -72,4 +72,16 @@ default String getExportModelName(Recommender aRecommender) T readTraits(Recommender aRecommender); void writeTraits(Recommender aRecommender, T aTraits); + + /** + * The task of a ranker is to provide ordered suggestions instead of scored suggestions. While a + * ranker will usually set the score property of a suggestion, that score should not be + * displayed prominently in the user interface. + * + * @return {@code true} if the recommender is a ranker + */ + default boolean isRanker(Recommender aRecommender) + { + return false; + } } 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 6b8fc15880d..a8ec17221b7 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 @@ -391,7 +391,7 @@ public List lookupLazyDetails(SourceDocument aDocument, User a var details = new VLazyDetailGroup(); for (var ao : sortedByScore) { var items = new ArrayList(); - if (ao.getScore() != -1) { + if (ao.getScore() > 0.0d) { items.add(String.format("Score: %.2f", ao.getScore())); } if (ao.getScoreExplanation().isPresent()) { diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java index 5d86854c7aa..51db6f0d826 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java @@ -23,6 +23,7 @@ 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.Type; @@ -108,6 +109,8 @@ public void render(VDocument aVDoc, RenderRequest aRequest, Predictions aPredict var features = annotationService.listSupportedFeatures(layer).stream() .collect(toMap(AnnotationFeature::getName, identity())); + var rankerCache = new HashMap(); + for (SuggestionGroup group : groupedPredictions) { for (RelationSuggestion suggestion : group.bestSuggestions(pref)) { @@ -142,9 +145,19 @@ public void render(VDocument aVDoc, RenderRequest aRequest, Predictions aPredict ? 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; + }); + VArc arc = new VArc(layer, suggestion.getVID(), VID.of(source), VID.of(target), "\uD83E\uDD16 " + suggestion.getUiLabel(), featureAnnotation, COLOR); arc.setScore(suggestion.getScore()); + arc.setHideScore(isRanker); aVDoc.add(arc); } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java index 69602bde33c..a08834715a6 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java @@ -21,6 +21,7 @@ import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; +import java.util.HashMap; import java.util.Map; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; @@ -102,6 +103,8 @@ public void render(VDocument vdoc, RenderRequest aRequest, Predictions aPredicti var features = annotationService.listSupportedFeatures(layer).stream() .collect(toMap(AnnotationFeature::getName, identity())); + var rankerCache = new HashMap(); + for (var suggestionGroup : groups) { // Render annotations for each label for (var suggestion : suggestionGroup.bestSuggestions(pref)) { @@ -120,9 +123,19 @@ public void render(VDocument vdoc, RenderRequest aRequest, Predictions aPredicti ? 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 v = new VSpan(layer, suggestion.getVID(), range.get(), featureAnnotation, COLOR); v.setScore(suggestion.getScore()); + v.setHideScore(isRanker); v.setActionButtons(recommenderProperties.isActionButtonsEnabled()); vdoc.add(v); From 8bab3b3a83e95b588bab3bdc3f45a6038731cc09 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Sun, 24 Dec 2023 11:14:11 +0100 Subject: [PATCH 03/13] #4292 - ollama-based recommender - Enable creation of document-level recommendations using ollama, e.g. for summarization --- inception/inception-imls-ollama/pom.xml | 8 ++++ .../imls/ollama/ExtractionMode.java | 15 ++++++- .../imls/ollama/ExtractionModeSelect.java | 37 +++++++++++++---- .../imls/ollama/OllamaRecommender.java | 5 +-- .../imls/ollama/OllamaRecommenderFactory.java | 6 ++- .../ollama/OllamaRecommenderTraitsEditor.java | 40 +++++++++++-------- .../imls/ollama/prompt/PromptingMode.java | 3 ++ .../response/ResponseAsLabelExtractor.java | 22 +++++++--- ...MetadataLayerSingletonCreatingWatcher.java | 8 ++-- ...umentMetadataAnnotationSelectionPanel.java | 11 ++--- .../api/model/LearningRecord.java | 4 +- .../api/recommender/AbstractTraitsEditor.java | 9 +---- 12 files changed, 111 insertions(+), 57 deletions(-) diff --git a/inception/inception-imls-ollama/pom.xml b/inception/inception-imls-ollama/pom.xml index 6df54c51e5b..0cb79e0de40 100644 --- a/inception/inception-imls-ollama/pom.xml +++ b/inception/inception-imls-ollama/pom.xml @@ -46,6 +46,14 @@ de.tudarmstadt.ukp.inception.app inception-api-render + + de.tudarmstadt.ukp.inception.app + inception-api-annotation + + + de.tudarmstadt.ukp.inception.app + inception-layer-docmetadata + de.tudarmstadt.ukp.inception.app inception-support diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/ExtractionMode.java b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/ExtractionMode.java index 272b4a2787d..875b6bb0c78 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/ExtractionMode.java +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/ExtractionMode.java @@ -19,11 +19,24 @@ import com.fasterxml.jackson.annotation.JsonProperty; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; + public enum ExtractionMode { @JsonProperty("response-as-label") RESPONSE_AS_LABEL, // @JsonProperty("mentions-from-json") - MENTIONS_FROM_JSON + MENTIONS_FROM_JSON; + + public boolean accepts(AnnotationLayer aLayer) + { + if (this == MENTIONS_FROM_JSON) { + // Mention extraction only makes sense for span layers + return SpanLayerSupport.TYPE.equals(aLayer.getType()); + } + + return true; + } } diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/ExtractionModeSelect.java b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/ExtractionModeSelect.java index de82000e2c3..cdac9922f3b 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/ExtractionModeSelect.java +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/ExtractionModeSelect.java @@ -19,32 +19,55 @@ import static java.util.Arrays.asList; +import java.util.Collections; + import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.EnumChoiceRenderer; import org.apache.wicket.model.IModel; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; + public class ExtractionModeSelect extends DropDownChoice { private static final long serialVersionUID = 1789605828488016006L; - public ExtractionModeSelect(String aId) - { - super(aId); - } + private IModel recommender; - public ExtractionModeSelect(String aId, IModel aModel) + public ExtractionModeSelect(String aId, IModel aModel, + IModel aRecommender) { super(aId); setModel(aModel); + recommender = aRecommender; } @Override protected void onInitialize() { super.onInitialize(); - setChoiceRenderer(new EnumChoiceRenderer<>(this)); - setChoices(asList(ExtractionMode.values())); + } + + @Override + protected void onConfigure() + { + super.onConfigure(); + + if (!recommender.isPresent().getObject()) { + setChoices(Collections.emptyList()); + return; + } + + var validChoices = asList(ExtractionMode.values()).stream() // + .filter(e -> e.accepts(recommender.getObject().getLayer())) // + .toList(); + setChoices(validChoices); + + if (validChoices.size() == 1) { + setModelObject(validChoices.get(0)); + } + + setVisible(validChoices.size() > 1); } } diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommender.java b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommender.java index f67526eda4d..591b705d5b1 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommender.java +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommender.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.lang.invoke.MethodHandles; import java.nio.charset.Charset; -import java.util.List; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.uima.cas.CAS; @@ -47,7 +46,6 @@ import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.prompt.PromptContext; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.prompt.PromptContextGenerator; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.response.MentionsFromJsonExtractor; -import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.response.MentionsSample; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.response.ResponseAsLabelExtractor; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.response.ResponseExtractor; import de.tudarmstadt.ukp.inception.rendering.model.Range; @@ -92,8 +90,7 @@ public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd throws RecommendationException { var responseExtractor = getResponseExtractor(); - List examples = responseExtractor.generate(this, aCas, - MAX_FEW_SHOT_EXAMPLES); + var examples = responseExtractor.generate(this, aCas, MAX_FEW_SHOT_EXAMPLES); getPromptContextGenerator().generate(this, aCas, aBegin, aEnd).forEach(promptContext -> { try { diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderFactory.java b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderFactory.java index 76ee8019800..9ea98a4d9e8 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderFactory.java +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderFactory.java @@ -17,7 +17,6 @@ */ package de.tudarmstadt.ukp.inception.recommendation.imls.ollama; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.SPAN_TYPE; import static org.apache.uima.cas.CAS.TYPE_NAME_STRING; import java.lang.invoke.MethodHandles; @@ -34,12 +33,14 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactoryImplBase; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.client.OllamaClient; import de.tudarmstadt.ukp.inception.support.io.WatchedResourceFile; import de.tudarmstadt.ukp.inception.support.yaml.YamlUtil; +import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; public class OllamaRecommenderFactory extends RecommendationEngineFactoryImplBase @@ -87,7 +88,8 @@ public RecommendationEngine build(Recommender aRecommender) @Override public boolean accepts(AnnotationLayer aLayer, AnnotationFeature aFeature) { - return SPAN_TYPE.equals(aFeature.getLayer().getType()) + return (SpanLayerSupport.TYPE.equals(aFeature.getLayer().getType()) + || DocumentMetadataLayerSupport.TYPE.equals(aFeature.getLayer().getType())) && TYPE_NAME_STRING.equals(aFeature.getType()); } diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.java b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.java index 4b59dd142e9..66a07858365 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.java +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.java @@ -17,6 +17,8 @@ */ package de.tudarmstadt.ukp.inception.recommendation.imls.ollama; +import static de.tudarmstadt.ukp.inception.support.lambda.HtmlElementEvents.CHANGE_EVENT; + import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -62,7 +64,7 @@ public class OllamaRecommenderTraitsEditor private @SpringBean RecommendationService recommendationService; private @SpringBean RecommendationEngineFactory toolFactory; - private final IModel traits; + private final CompoundPropertyModel traits; private final WebMarkupContainer optionSettingsContainer; private final IModel> optionSettings; @@ -93,18 +95,8 @@ protected void onSubmit() presetSelect.setModel(Model.of()); presetSelect.setChoiceRenderer(new ChoiceRenderer<>("name")); presetSelect.setChoices(aPresets); - presetSelect.add(new LambdaAjaxFormComponentUpdatingBehavior("change", _target -> { - var preset = presetSelect.getModelObject(); - if (preset != null) { - var settings = traits.getObject(); - settings.setPrompt(preset.getPrompt()); - settings.setExtractionMode(preset.getExtractionMode()); - settings.setFormat(preset.getFormat()); - settings.setPromptingMode(preset.getPromptingMode()); - settings.setRaw(preset.isRaw()); - } - _target.add(form); - })); + presetSelect.add(new LambdaAjaxFormComponentUpdatingBehavior(CHANGE_EVENT, + _target -> applyPreset(form, presetSelect.getModelObject(), _target))); form.add(presetSelect); form.add(new TextField("url")); @@ -116,10 +108,10 @@ protected void onSubmit() markdownLabel.setOutputMarkupId(true); form.add(markdownLabel); form.add(new PromptingModeSelect("promptingMode") - .add(new LambdaAjaxFormComponentUpdatingBehavior("change", _target -> { - _target.add(markdownLabel); - }))); - form.add(new ExtractionModeSelect("extractionMode")); + .add(new LambdaAjaxFormComponentUpdatingBehavior(CHANGE_EVENT, + _target -> _target.add(markdownLabel)))); + form.add(new ExtractionModeSelect("extractionMode", traits.bind("extractionMode"), + getModel())); form.add(new OllamaResponseFormatSelect("format")); add(form); @@ -144,6 +136,20 @@ protected void onSubmit() optionSettingsContainer.add(createOptionSettingsList("optionSettings", optionSettings)); } + private void applyPreset(Form aForm, Preset aPreset, + AjaxRequestTarget aTarget) + { + if (aPreset != null) { + var settings = traits.getObject(); + settings.setPrompt(aPreset.getPrompt()); + settings.setExtractionMode(aPreset.getExtractionMode()); + settings.setFormat(aPreset.getFormat()); + settings.setPromptingMode(aPreset.getPromptingMode()); + settings.setRaw(aPreset.isRaw()); + } + aTarget.add(aForm); + } + private ListView createOptionSettingsList(String aId, IModel> aOptionSettings) { diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/prompt/PromptingMode.java b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/prompt/PromptingMode.java index dc54330b650..414f5a3868c 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/prompt/PromptingMode.java +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/prompt/PromptingMode.java @@ -24,6 +24,7 @@ public enum PromptingMode @JsonProperty("per-annotation") PER_ANNOTATION(""" Template variables: + * `text`: annotation text, * `sentence`: sentence containing annotation, * `examples`: labeled annotations"""), @@ -31,12 +32,14 @@ public enum PromptingMode @JsonProperty("per-sentence") PER_SENTENCE(""" Template variables: + * `text`: sentence text, * `examples`: labeled annotations"""), @JsonProperty("per-document") PER_DOCUMENT(""" Template variables: + * `text`: document text"""); private final String hints; diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/response/ResponseAsLabelExtractor.java b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/response/ResponseAsLabelExtractor.java index 369636cc8f8..9979c9e92f3 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/response/ResponseAsLabelExtractor.java +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/response/ResponseAsLabelExtractor.java @@ -48,12 +48,22 @@ public void extract(RecommendationEngine aEngine, CAS aCas, PromptContext aConte var predictedFeature = aEngine.getPredictedFeature(aCas); var isPredictionFeature = aEngine.getIsPredictionFeature(aCas); - var prediction = aCas.createAnnotation(predictedType, candidate.getBegin(), - candidate.getEnd()); - prediction.setFeatureValueFromString(predictedFeature, aResponse); - prediction.setBooleanValue(isPredictionFeature, true); - aCas.addFsToIndexes(prediction); + if (aCas.getAnnotationType().subsumes(predictedType)) { + var prediction = aCas.createAnnotation(predictedType, candidate.getBegin(), + candidate.getEnd()); + prediction.setFeatureValueFromString(predictedFeature, aResponse); + prediction.setBooleanValue(isPredictionFeature, true); + aCas.addFsToIndexes(prediction); - LOG.debug("Prediction generated [{}] -> [{}]", prediction.getCoveredText(), aResponse); + LOG.debug("Prediction generated: [{}] -> [{}]", prediction.getCoveredText(), aResponse); + } + else { + var prediction = aCas.createFS(predictedType); + prediction.setFeatureValueFromString(predictedFeature, aResponse); + prediction.setBooleanValue(isPredictionFeature, true); + aCas.addFsToIndexes(prediction); + + LOG.debug("Prediction generated: doc -> [{}]", aResponse); + } } } diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/layer/DocumentMetadataLayerSingletonCreatingWatcher.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/layer/DocumentMetadataLayerSingletonCreatingWatcher.java index e77610f847e..8f3e48e8342 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/layer/DocumentMetadataLayerSingletonCreatingWatcher.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/layer/DocumentMetadataLayerSingletonCreatingWatcher.java @@ -19,7 +19,6 @@ import java.io.IOException; import java.util.List; -import java.util.stream.Collectors; import org.apache.uima.cas.CAS; import org.springframework.context.event.EventListener; @@ -59,7 +58,7 @@ private List listMetadataLayers(Project aProject) return annotationService.listAnnotationLayer(aProject).stream() .filter(layer -> DocumentMetadataLayerSupport.TYPE.equals(layer.getType()) && layer.isEnabled()) - .collect(Collectors.toList()); + .toList(); } @EventListener @@ -72,13 +71,12 @@ public void onBeforeDocumentOpenedEvent(BeforeDocumentOpenedEvent aEvent) } CAS cas = aEvent.getCas(); - for (AnnotationLayer layer : listMetadataLayers(aEvent.getDocument().getProject())) { + for (var layer : listMetadataLayers(aEvent.getDocument().getProject())) { if (!getLayerSupport(layer).readTraits(layer).isSingleton()) { continue; } - DocumentMetadataLayerAdapter adapter = (DocumentMetadataLayerAdapter) annotationService - .getAdapter(layer); + var adapter = (DocumentMetadataLayerAdapter) annotationService.getAdapter(layer); if (cas.select(adapter.getAnnotationType(cas)).isEmpty()) { adapter.add(aEvent.getDocument(), aEvent.getSessionOwner(), cas); } diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/DocumentMetadataAnnotationSelectionPanel.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/DocumentMetadataAnnotationSelectionPanel.java index 1b4ba3e0a45..fd0b616466b 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/DocumentMetadataAnnotationSelectionPanel.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/DocumentMetadataAnnotationSelectionPanel.java @@ -147,6 +147,7 @@ public DocumentMetadataAnnotationSelectionPanel(String aId, CasProvider aCasProv var availableLayers = LoadableDetachableModel.of(this::listCreatableMetadataLayers); var content = new WebMarkupContainer("content"); + content.add(visibleWhenNot(layers.map(List::isEmpty).orElse(true))); add(content); var layer = new DropDownChoice(CID_LAYER); @@ -164,11 +165,10 @@ public DocumentMetadataAnnotationSelectionPanel(String aId, CasProvider aCasProv layersContainer = new WebMarkupContainer(CID_LAYERS_CONTAINER); layersContainer.setOutputMarkupPlaceholderTag(true); layersContainer.add(createLayerGroupList()); - layersContainer.add(visibleWhenNot(availableLayers.map(List::isEmpty).orElse(true))); content.add(layersContainer); add(new WebMarkupContainer(CID_NO_LAYERS_WARNING) - .add(visibleWhen(availableLayers.map(List::isEmpty).orElse(true)))); + .add(visibleWhen(layers.map(List::isEmpty).orElse(true)))); } private void actionAccept(AjaxRequestTarget aTarget, AnnotationListItem aItem) @@ -314,8 +314,6 @@ private void actionSelect(AjaxRequestTarget aTarget, WebMarkupContainer containe private ListView createLayerGroupList() { - var availableLayers = listCreatableMetadataLayers(); - return new ListView(CID_LAYERS, layers) { private static final long serialVersionUID = 1L; @@ -331,7 +329,6 @@ protected void populateItem(ListItem aItem) var container = new WebMarkupContainer(CID_ANNOTATIONS_CONTAINER); container.setOutputMarkupPlaceholderTag(true); container.add(createAnnotationList(annotations)); - container.add(visibleWhen(() -> !availableLayers.isEmpty())); aItem.add(container); } @@ -521,8 +518,8 @@ private void generateSuggestionItems(AnnotationLayer aLayer, var featureSupport = fsRegistry.findExtension(feature).orElseThrow(); var annotation = featureSupport.renderFeatureValue(feature, suggestion.getLabel()); - items.add(new AnnotationListItem(suggestion.getVID(), annotation, aLayer, - singleton, suggestion.getScore())); + items.add(new AnnotationListItem(suggestion.getVID(), annotation, aLayer, false, + suggestion.getScore())); } } } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LearningRecord.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LearningRecord.java index 7235682fd3e..e943076015e 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LearningRecord.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LearningRecord.java @@ -50,6 +50,7 @@ public class LearningRecord { private static final long serialVersionUID = -8487663728083806672L; private static final int TOKEN_TEXT_LENGTH = 255; + private static final int LABEL_LENGTH = 255; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -267,7 +268,8 @@ public String getAnnotation() public void setAnnotation(String aAnnotation) { - annotation = aAnnotation; + int targetLength = Math.min(aAnnotation.length(), LABEL_LENGTH); + annotation = aAnnotation.substring(0, targetLength); } public LearningRecordUserAction getUserAction() diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/AbstractTraitsEditor.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/AbstractTraitsEditor.java index d408fbc9432..1c7f25ee4f7 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/AbstractTraitsEditor.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/AbstractTraitsEditor.java @@ -17,7 +17,7 @@ */ package de.tudarmstadt.ukp.inception.recommendation.api.recommender; -import org.apache.wicket.markup.html.panel.Panel; +import org.apache.wicket.markup.html.panel.GenericPanel; import org.apache.wicket.model.IModel; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; @@ -40,7 +40,7 @@ * limitations under the License. */ public class AbstractTraitsEditor - extends Panel + extends GenericPanel { private static final long serialVersionUID = -5826029092354401342L; @@ -48,9 +48,4 @@ public AbstractTraitsEditor(String aId, IModel aRecommender) { super(aId, aRecommender); } - - public Recommender getModelObject() - { - return (Recommender) getDefaultModelObject(); - } } From 43346f4ff3fb926cdb4d1b0e02aadc1d95e9434e Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Sun, 24 Dec 2023 16:37:32 +0100 Subject: [PATCH 04/13] #4292 - ollama-based recommender - Allow logging messages during prediction - Query models from the ollama deamon --- .../recommender/NamedEntityLinker.java | 3 +- .../conceptlinking/NamedEntityLinkerTest.java | 3 +- .../DataMajorityNerRecommender.java | 3 +- .../DataMajorityNerRecommenderTest.java | 3 +- .../imls/elg/ElgRecommender.java | 4 +- .../imls/external/v1/ExternalRecommender.java | 3 +- .../ExternalRecommenderIntegrationTest.java | 5 +- ...ockRemoteStringMatchingNerRecommender.java | 3 +- .../recommendation/imls/hf/HfRecommender.java | 4 +- .../imls/lapps/LappsGridRecommender.java | 3 +- .../LappsRecommenderIntegrationTest.java | 3 +- inception/inception-imls-ollama/pom.xml | 4 + .../imls/ollama/OllamaRecommender.java | 6 +- .../ollama/OllamaRecommenderTraitsEditor.html | 9 +- .../ollama/OllamaRecommenderTraitsEditor.java | 48 +++++++- .../imls/ollama/client/OllamaClientImpl.java | 4 +- .../imls/ollama/OllamaRecommenderTest.java | 11 +- .../doccat/OpenNlpDoccatRecommender.java | 3 +- .../opennlp/ner/OpenNlpNerRecommender.java | 3 +- .../opennlp/pos/OpenNlpPosRecommender.java | 3 +- .../OpenNlpDoccatMetadataRecommenderTest.java | 3 +- .../doccat/OpenNlpDoccatRecommenderTest.java | 3 +- .../ner/OpenNlpNerRecommenderTest.java | 3 +- .../pos/OpenNlpPosRecommenderTest.java | 3 +- .../StringMatchingRelationRecommender.java | 3 +- .../span/StringMatchingRecommender.java | 3 +- ...StringMatchingRelationRecommenderTest.java | 3 +- .../span/StringMatchingRecommenderTest.java | 13 ++- .../imls/weblicht/WeblichtRecommender.java | 3 +- .../api/recommender/PredictionContext.java | 107 ++++++++++++++++++ .../api/recommender/RecommendationEngine.java | 4 +- .../api/recommender/RecommenderContext.java | 3 - .../service/RecommendationServiceImpl.java | 12 +- 33 files changed, 238 insertions(+), 53 deletions(-) create mode 100644 inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/PredictionContext.java diff --git a/inception/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/recommender/NamedEntityLinker.java b/inception/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/recommender/NamedEntityLinker.java index 1ec839a07da..73d26263448 100644 --- a/inception/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/recommender/NamedEntityLinker.java +++ b/inception/inception-concept-linking/src/main/java/de/tudarmstadt/ukp/inception/conceptlinking/recommender/NamedEntityLinker.java @@ -43,6 +43,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.DataSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -90,7 +91,7 @@ public void train(RecommenderContext aContext, List aCasList) } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { Type predictedType = getPredictedType(aCas); diff --git a/inception/inception-concept-linking/src/test/java/de/tudarmstadt/ukp/inception/conceptlinking/NamedEntityLinkerTest.java b/inception/inception-concept-linking/src/test/java/de/tudarmstadt/ukp/inception/conceptlinking/NamedEntityLinkerTest.java index 8f5f49ba4ab..0fbe22f3649 100644 --- a/inception/inception-concept-linking/src/test/java/de/tudarmstadt/ukp/inception/conceptlinking/NamedEntityLinkerTest.java +++ b/inception/inception-concept-linking/src/test/java/de/tudarmstadt/ukp/inception/conceptlinking/NamedEntityLinkerTest.java @@ -65,6 +65,7 @@ import de.tudarmstadt.ukp.inception.kb.graph.KBHandle; import de.tudarmstadt.ukp.inception.kb.model.KnowledgeBase; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupport; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; @@ -146,7 +147,7 @@ public void thatPredictionWorks() throws Exception sut.train(context, Collections.singletonList(cas)); RecommenderTestHelper.addScoreFeature(cas, NamedEntity.class, "value"); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); List predictions = getPredictions(cas, NamedEntity.class); diff --git a/inception/inception-example-imls-data-majority/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/datamajority/DataMajorityNerRecommender.java b/inception/inception-example-imls-data-majority/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/datamajority/DataMajorityNerRecommender.java index 879ea387eb5..028d8790828 100644 --- a/inception/inception-example-imls-data-majority/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/datamajority/DataMajorityNerRecommender.java +++ b/inception/inception-example-imls-data-majority/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/datamajority/DataMajorityNerRecommender.java @@ -42,6 +42,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.LabelPair; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -133,7 +134,7 @@ private DataMajorityModel trainModel(List aAnnotations) // tag::predict1[] @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { DataMajorityModel model = aContext.get(KEY_MODEL).orElseThrow( diff --git a/inception/inception-example-imls-data-majority/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/datamajority/DataMajorityNerRecommenderTest.java b/inception/inception-example-imls-data-majority/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/datamajority/DataMajorityNerRecommenderTest.java index 6a8903af83c..ba9e415527a 100644 --- a/inception/inception-example-imls-data-majority/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/datamajority/DataMajorityNerRecommenderTest.java +++ b/inception/inception-example-imls-data-majority/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/datamajority/DataMajorityNerRecommenderTest.java @@ -59,6 +59,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.IncrementalSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.PercentageBasedSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.support.test.recommendation.DkproTestHelper; import de.tudarmstadt.ukp.inception.support.test.recommendation.RecommenderTestHelper; @@ -104,7 +105,7 @@ public void thatPredictionWorks() throws Exception sut.train(context, asList(cas)); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); Collection predictions = getPredictions(cas, NamedEntity.class); diff --git a/inception/inception-imls-elg/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/elg/ElgRecommender.java b/inception/inception-imls-elg/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/elg/ElgRecommender.java index fb16da8c37f..2c5eaa06dff 100644 --- a/inception/inception-imls-elg/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/elg/ElgRecommender.java +++ b/inception/inception-imls-elg/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/elg/ElgRecommender.java @@ -34,8 +34,8 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.NonTrainableRecommenderEngineImplBase; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; -import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.recommendation.imls.elg.model.ElgAnnotation; import de.tudarmstadt.ukp.inception.recommendation.imls.elg.model.ElgAnnotationsResponse; import de.tudarmstadt.ukp.inception.recommendation.imls.elg.model.ElgServiceResponse; @@ -67,7 +67,7 @@ public ElgRecommender(Recommender aRecommender, ElgRecommenderTraits aTraits, } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { ElgServiceResponse response; diff --git a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommender.java b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommender.java index 25bae96ed48..5d389e11cdb 100644 --- a/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommender.java +++ b/inception/inception-imls-external/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommender.java @@ -64,6 +64,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.DataSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -216,7 +217,7 @@ else if (response.statusCode() >= HTTP_BAD_REQUEST) { } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { var client = getClient(); diff --git a/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderIntegrationTest.java b/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderIntegrationTest.java index 742f89695dd..606049ab412 100644 --- a/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderIntegrationTest.java +++ b/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/ExternalRecommenderIntegrationTest.java @@ -57,6 +57,7 @@ import de.tudarmstadt.ukp.inception.annotation.storage.CasMetadataUtils; import de.tudarmstadt.ukp.inception.annotation.storage.CasStorageSession; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.recommendation.imls.external.v1.config.ExternalRecommenderPropertiesImpl; import de.tudarmstadt.ukp.inception.recommendation.imls.external.v1.messages.PredictionRequest; @@ -133,7 +134,7 @@ public void thatPredictingWorks() throws Exception var cas = casses.get(0); RecommenderTestHelper.addScoreFeature(cas, NamedEntity.class, "value"); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); var predictions = getPredictions(cas, NamedEntity.class); @@ -180,7 +181,7 @@ public void thatPredictingSendsCorrectRequest() throws Exception var cas = casses.get(0); RecommenderTestHelper.addScoreFeature(cas, NamedEntity.class, "value"); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); var request = fromJsonString(PredictionRequest.class, requestBodies.get(1)); diff --git a/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/MockRemoteStringMatchingNerRecommender.java b/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/MockRemoteStringMatchingNerRecommender.java index 07046edfb16..4a78caf2390 100644 --- a/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/MockRemoteStringMatchingNerRecommender.java +++ b/inception/inception-imls-external/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/external/v1/MockRemoteStringMatchingNerRecommender.java @@ -40,6 +40,7 @@ import org.xml.sax.SAXException; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.recommendation.imls.external.v1.messages.PredictionRequest; @@ -93,7 +94,7 @@ public String predict(String aPredictionRequestJson) } } - recommendationEngine.predict(context, cas); + recommendationEngine.predict(new PredictionContext(context), cas); return buildPredictionResponse(cas); } diff --git a/inception/inception-imls-hf/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/hf/HfRecommender.java b/inception/inception-imls-hf/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/hf/HfRecommender.java index 8e2b8c9cf22..5808b0998dc 100644 --- a/inception/inception-imls-hf/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/hf/HfRecommender.java +++ b/inception/inception-imls-hf/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/hf/HfRecommender.java @@ -29,8 +29,8 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.NonTrainableRecommenderEngineImplBase; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; -import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.recommendation.imls.hf.client.HfInferenceClient; import de.tudarmstadt.ukp.inception.recommendation.imls.hf.model.HfEntityGroup; import de.tudarmstadt.ukp.inception.rendering.model.Range; @@ -53,7 +53,7 @@ public HfRecommender(Recommender aRecommender, HfRecommenderTraits aTraits, } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { List response; diff --git a/inception/inception-imls-lapps/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/lapps/LappsGridRecommender.java b/inception/inception-imls-lapps/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/lapps/LappsGridRecommender.java index ac9e0984850..36f69a6a8a2 100644 --- a/inception/inception-imls-lapps/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/lapps/LappsGridRecommender.java +++ b/inception/inception-imls-lapps/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/lapps/LappsGridRecommender.java @@ -39,6 +39,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.DataSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -67,7 +68,7 @@ public void train(RecommenderContext aContext, List aCasses) } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { // FIXME: Ignores begin/end - always fetches predictions for the entire CAS diff --git a/inception/inception-imls-lapps/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/lapps/LappsRecommenderIntegrationTest.java b/inception/inception-imls-lapps/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/lapps/LappsRecommenderIntegrationTest.java index 8259fc3f604..651d3e6048c 100644 --- a/inception/inception-imls-lapps/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/lapps/LappsRecommenderIntegrationTest.java +++ b/inception/inception-imls-lapps/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/lapps/LappsRecommenderIntegrationTest.java @@ -42,6 +42,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.dkpro.core.api.lexmorph.type.pos.POS; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.recommendation.imls.lapps.traits.LappsGridRecommenderTraits; import okhttp3.mockwebserver.MockResponse; @@ -83,7 +84,7 @@ public void thatPredictingPosWorks() throws Exception RecommenderContext context = new RecommenderContext(); CAS cas = loadData(); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); Collection predictions = JCasUtil.select(cas.getJCas(), POS.class); diff --git a/inception/inception-imls-ollama/pom.xml b/inception/inception-imls-ollama/pom.xml index 0cb79e0de40..8feea34cbc1 100644 --- a/inception/inception-imls-ollama/pom.xml +++ b/inception/inception-imls-ollama/pom.xml @@ -104,6 +104,10 @@ org.apache.wicket wicket-spring + + com.googlecode.wicket-jquery-ui + wicket-kendo-ui + org.danekja jdk-serializable-functional diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommender.java b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommender.java index 591b705d5b1..d356f463684 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommender.java +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommender.java @@ -36,8 +36,8 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.NonTrainableRecommenderEngineImplBase; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; -import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.client.OllamaClient; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.client.OllamaGenerateRequest; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.prompt.PerAnnotationContextGenerator; @@ -86,7 +86,7 @@ public String getString(String aFullName, Charset aEncoding, } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { var responseExtractor = getResponseExtractor(); @@ -103,6 +103,8 @@ public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd responseExtractor.extract(this, aCas, promptContext, response); } catch (IOException e) { + aContext.error("Ollama [%s] failed to respond: %s", traits.getModel(), + ExceptionUtils.getRootCauseMessage(e)); LOG.error("Ollama [{}] failed to respond: {}", traits.getModel(), ExceptionUtils.getRootCauseMessage(e)); } diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.html b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.html index 010b3773ffe..5f6080ca20b 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.html +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.html @@ -17,6 +17,13 @@ --> + + +
@@ -32,7 +39,7 @@
- +
diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.java b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.java index 66a07858365..fb153bb34e7 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.java +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTraitsEditor.java @@ -18,8 +18,12 @@ package de.tudarmstadt.ukp.inception.recommendation.imls.ollama; import static de.tudarmstadt.ukp.inception.support.lambda.HtmlElementEvents.CHANGE_EVENT; +import static de.tudarmstadt.ukp.inception.support.wicket.WicketUtil.wrapInTryCatch; +import static java.lang.String.format; +import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -41,17 +45,26 @@ import org.apache.wicket.model.Model; import org.apache.wicket.model.PropertyModel; import org.apache.wicket.model.util.ListModel; +import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.spring.injection.annot.SpringBean; +import org.apache.wicket.validation.validator.UrlValidator; + +import com.googlecode.wicket.kendo.ui.KendoUIBehavior; +import com.googlecode.wicket.kendo.ui.form.combobox.ComboBox; +import com.googlecode.wicket.kendo.ui.form.combobox.ComboBoxBehavior; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.AbstractTraitsEditor; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory; +import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.client.OllamaClientImpl; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.client.OllamaGenerateRequest; +import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.client.OllamaModel; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.client.Option; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxFormComponentUpdatingBehavior; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxLink; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxSubmitLink; +import de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior; import de.tudarmstadt.ukp.inception.support.markdown.MarkdownLabel; public class OllamaRecommenderTraitsEditor @@ -99,8 +112,23 @@ protected void onSubmit() _target -> applyPreset(form, presetSelect.getModelObject(), _target))); form.add(presetSelect); - form.add(new TextField("url")); - form.add(new TextField("model")); + var modelsModel = LoadableDetachableModel.of(this::listModels); + var model = new ComboBox("model", modelsModel); + model.add(LambdaBehavior.onConfigure(() -> { + // Trigger a re-loading of the tagset from the server as constraints may have + // changed the ordering + modelsModel.detach(); + var target = RequestCycle.get().find(AjaxRequestTarget.class); + if (target.isPresent()) { + target.get().appendJavaScript(wrapInTryCatch(format( // + "var $w = %s; if ($w) { $w.dataSource.read(); }", + KendoUIBehavior.widget(this, ComboBoxBehavior.METHOD)))); + } + })); + model.setOutputMarkupId(true); + form.add(model); + form.add(new TextField("url").add(new LambdaAjaxFormComponentUpdatingBehavior( + CHANGE_EVENT, _target -> _target.add(model)))); form.add(new TextArea("prompt")); form.add(new CheckBox("raw").setOutputMarkupPlaceholderTag(true)); var markdownLabel = new MarkdownLabel("promptHints", @@ -189,4 +217,20 @@ private String getPromptHints() { return traits.getObject().getPromptingMode().getHints(); } + + private List listModels() + { + var url = traits.map(OllamaRecommenderTraits::getUrl).orElse(null).getObject(); + if (!new UrlValidator(new String[] { "http", "https" }).isValid(url)) { + return Collections.emptyList(); + } + + var client = new OllamaClientImpl(); + try { + return client.listModels(url).stream().map(OllamaModel::getName).toList(); + } + catch (IOException e) { + return Collections.emptyList(); + } + } } diff --git a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/client/OllamaClientImpl.java b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/client/OllamaClientImpl.java index 4aba17643ee..7dcf76b4fc3 100644 --- a/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/client/OllamaClientImpl.java +++ b/inception/inception-imls-ollama/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/client/OllamaClientImpl.java @@ -156,8 +156,8 @@ public List listModels(String aUrl) throws IOException private void handleError(HttpResponse response) throws IOException { if (response.statusCode() >= HTTP_BAD_REQUEST) { - String responseBody = getResponseBody(response); - String msg = format("Request was not successful: [%d] - [%s]", response.statusCode(), + var responseBody = getResponseBody(response); + var msg = format("Request was not successful: [%d] - [%s]", response.statusCode(), responseBody); throw new IOException(msg); } diff --git a/inception/inception-imls-ollama/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTest.java b/inception/inception-imls-ollama/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTest.java index ee8efa70574..85dd26c35ea 100644 --- a/inception/inception-imls-ollama/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTest.java +++ b/inception/inception-imls-ollama/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/ollama/OllamaRecommenderTest.java @@ -48,6 +48,7 @@ import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderTypeSystemUtils; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.recommendation.imls.ollama.client.OllamaClientImpl; import de.tudarmstadt.ukp.inception.support.test.http.HttpTestUtils; @@ -95,7 +96,7 @@ void testPerDocumentUsingReponseAsLabel() throws Exception traits.setExtractionMode(ExtractionMode.RESPONSE_AS_LABEL); var sut = new OllamaRecommender(recommender, traits, new OllamaClientImpl()); - sut.predict(new RecommenderContext(), cas); + sut.predict(new PredictionContext(new RecommenderContext()), cas); var predictions = cas.select(NamedEntity.class) .filter(ne -> getFeature(ne, FEATURE_NAME_IS_PREDICTION, Boolean.class)).toList(); @@ -120,7 +121,7 @@ void testPerDocumentUsingMentionsFromJsonList_Numbers() throws Exception traits.setExtractionMode(MENTIONS_FROM_JSON); var sut = new OllamaRecommender(recommender, traits, new OllamaClientImpl()); - sut.predict(new RecommenderContext(), cas); + sut.predict(new PredictionContext(new RecommenderContext()), cas); var predictions = cas.select(NamedEntity.class) .filter(ne -> getFeature(ne, FEATURE_NAME_IS_PREDICTION, Boolean.class)).toList(); @@ -143,7 +144,7 @@ void testPerDocumentUsingMentionsFromJsonList_Entities() throws Exception traits.setExtractionMode(MENTIONS_FROM_JSON); var sut = new OllamaRecommender(recommender, traits, new OllamaClientImpl()); - sut.predict(new RecommenderContext(), cas); + sut.predict(new PredictionContext(new RecommenderContext()), cas); var predictions = cas.select(NamedEntity.class) .filter(ne -> getFeature(ne, FEATURE_NAME_IS_PREDICTION, Boolean.class)).toList(); @@ -171,7 +172,7 @@ void testPerDocumentUsingMentionsFromJsonList_Politicians() throws Exception traits.setExtractionMode(MENTIONS_FROM_JSON); var sut = new OllamaRecommender(recommender, traits, new OllamaClientImpl()); - sut.predict(new RecommenderContext(), cas); + sut.predict(new PredictionContext(new RecommenderContext()), cas); var predictions = cas.select(NamedEntity.class) .filter(ne -> getFeature(ne, FEATURE_NAME_IS_PREDICTION, Boolean.class)).toList(); @@ -220,7 +221,7 @@ void testPerSentenceUsingMentionsFromJsonList_Politicians_fewShjot() throws Exce traits.setExtractionMode(MENTIONS_FROM_JSON); var sut = new OllamaRecommender(recommender, traits, new OllamaClientImpl()); - sut.predict(new RecommenderContext(), cas); + sut.predict(new PredictionContext(new RecommenderContext()), cas); var predictions = cas.select(NamedEntity.class) .filter(ne -> getFeature(ne, FEATURE_NAME_IS_PREDICTION, Boolean.class)).toList(); diff --git a/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatRecommender.java b/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatRecommender.java index 6ee353db93f..115b1180055 100644 --- a/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatRecommender.java +++ b/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatRecommender.java @@ -49,6 +49,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.LabelPair; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -137,7 +138,7 @@ public TrainingCapability getTrainingCapability() } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { var model = aContext.get(KEY_MODEL).orElseThrow( diff --git a/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/ner/OpenNlpNerRecommender.java b/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/ner/OpenNlpNerRecommender.java index 50d7a1a432a..54aa3c79ce2 100644 --- a/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/ner/OpenNlpNerRecommender.java +++ b/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/ner/OpenNlpNerRecommender.java @@ -43,6 +43,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.LabelPair; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -118,7 +119,7 @@ public TrainingCapability getTrainingCapability() } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { var model = aContext.get(KEY_MODEL).orElseThrow( diff --git a/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/pos/OpenNlpPosRecommender.java b/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/pos/OpenNlpPosRecommender.java index 23fe7bac5ce..7a67d4d6552 100644 --- a/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/pos/OpenNlpPosRecommender.java +++ b/inception/inception-imls-opennlp/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/pos/OpenNlpPosRecommender.java @@ -48,6 +48,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.LabelPair; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -119,7 +120,7 @@ public TrainingCapability getTrainingCapability() } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { var model = aContext.get(KEY_MODEL).orElseThrow( diff --git a/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatMetadataRecommenderTest.java b/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatMetadataRecommenderTest.java index 61b2e6bbc70..fdfc2ef5fb1 100644 --- a/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatMetadataRecommenderTest.java +++ b/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatMetadataRecommenderTest.java @@ -39,6 +39,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderTypeSystemUtils; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.PercentageBasedSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.support.uima.SegmentationUtils; import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; @@ -107,7 +108,7 @@ public void thatPredictionWorks() throws Exception sut.train(context, casList); var predictionCas = makePredictionCas("I like cars.", feature); - sut.predict(context, predictionCas); + sut.predict(new PredictionContext(context), predictionCas); var predictions = getPredictionFSes(predictionCas, layer.getName()); diff --git a/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatRecommenderTest.java b/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatRecommenderTest.java index 79b43de5c00..0694e1952a4 100644 --- a/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatRecommenderTest.java +++ b/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/doccat/OpenNlpDoccatRecommenderTest.java @@ -53,6 +53,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.IncrementalSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.PercentageBasedSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.support.test.recommendation.DkproTestHelper; import de.tudarmstadt.ukp.inception.support.test.recommendation.RecommenderTestHelper; @@ -103,7 +104,7 @@ public void thatPredictionWorks() throws Exception sut.train(context, asList(cas)); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); var predictions = getPredictions(cas, NamedEntity.class); diff --git a/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/ner/OpenNlpNerRecommenderTest.java b/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/ner/OpenNlpNerRecommenderTest.java index 32da4e7f8f1..13f8e279a54 100644 --- a/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/ner/OpenNlpNerRecommenderTest.java +++ b/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/ner/OpenNlpNerRecommenderTest.java @@ -53,6 +53,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.IncrementalSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.PercentageBasedSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.support.test.recommendation.DkproTestHelper; import de.tudarmstadt.ukp.inception.support.test.recommendation.RecommenderTestHelper; @@ -103,7 +104,7 @@ public void thatPredictionWorks() throws Exception sut.train(context, asList(cas)); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); Collection predictions = JCasUtil.select(cas.getJCas(), NamedEntity.class); diff --git a/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/pos/OpenNlpPosRecommenderTest.java b/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/pos/OpenNlpPosRecommenderTest.java index 2c6f1d5a9f7..5d0c7a0ea9d 100644 --- a/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/pos/OpenNlpPosRecommenderTest.java +++ b/inception/inception-imls-opennlp/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/opennlp/pos/OpenNlpPosRecommenderTest.java @@ -47,6 +47,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.IncrementalSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.PercentageBasedSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.support.test.recommendation.DkproTestHelper; import de.tudarmstadt.ukp.inception.support.test.recommendation.RecommenderTestHelper; @@ -97,7 +98,7 @@ public void thatPredictionWorks() throws Exception sut.train(context, asList(cas)); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); List predictions = RecommenderTestHelper.getPredictions(cas, POS.class); 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 a3479b751b5..8fc35f0f5ba 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 @@ -50,6 +50,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.LabelPair; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -95,7 +96,7 @@ private MultiValuedMap, String> trainModel(List aDa } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { MultiValuedMap, String> model = aContext.get(KEY_MODEL).orElseThrow( diff --git a/inception/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/span/StringMatchingRecommender.java b/inception/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/span/StringMatchingRecommender.java index e2d569dea22..294a9b6d5e2 100644 --- a/inception/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/span/StringMatchingRecommender.java +++ b/inception/inception-imls-stringmatch/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/span/StringMatchingRecommender.java @@ -58,6 +58,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.LabelPair; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -198,7 +199,7 @@ public void train(RecommenderContext aContext, List aCasses) throws Recomme } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { Trie dict = aContext.get(KEY_MODEL).orElseThrow( diff --git a/inception/inception-imls-stringmatch/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/relation/StringMatchingRelationRecommenderTest.java b/inception/inception-imls-stringmatch/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/relation/StringMatchingRelationRecommenderTest.java index 26610cda11b..4e8f4d4b256 100644 --- a/inception/inception-imls-stringmatch/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/relation/StringMatchingRelationRecommenderTest.java +++ b/inception/inception-imls-stringmatch/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/relation/StringMatchingRelationRecommenderTest.java @@ -45,6 +45,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.inception.annotation.storage.CasStorageSession; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.support.test.recommendation.RecommenderTestHelper; @@ -94,7 +95,7 @@ public void thatPredictingWorks() throws Exception CAS cas = loadSimpleCas(); sut.train(context, Collections.singletonList(cas)); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); List predictions = getPredictions(cas, RELATION_LAYER); diff --git a/inception/inception-imls-stringmatch/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/span/StringMatchingRecommenderTest.java b/inception/inception-imls-stringmatch/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/span/StringMatchingRecommenderTest.java index fa32d637b3c..14503e3df20 100644 --- a/inception/inception-imls-stringmatch/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/span/StringMatchingRecommenderTest.java +++ b/inception/inception-imls-stringmatch/src/test/java/de/tudarmstadt/ukp/inception/recommendation/imls/stringmatch/span/StringMatchingRecommenderTest.java @@ -63,6 +63,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.IncrementalSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.PercentageBasedSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; import de.tudarmstadt.ukp.inception.recommendation.imls.stringmatch.span.gazeteer.model.GazeteerEntry; import de.tudarmstadt.ukp.inception.support.test.recommendation.DkproTestHelper; @@ -116,7 +117,7 @@ public void thatPredictionWorks() throws Exception sut.train(context, Collections.singletonList(cas)); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); List predictions = getPredictions(cas, NamedEntity.class); @@ -141,7 +142,7 @@ public void thatPredictionForNoLabelAnnosWorks() throws Exception sut.train(context, Collections.singletonList(cas)); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); List predictions = getPredictions(cas, NamedEntity.class); @@ -170,7 +171,7 @@ public void thatPredictionForCharacterLevelLayerWorks() throws Exception sut.pretrain(gazeteer, context); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); List predictions = getPredictions(cas, NamedEntity.class); @@ -198,7 +199,7 @@ public void thatPredictionForCrossSentenceLayerWorks() throws Exception sut.pretrain(gazeteer, context); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); List predictions = getPredictions(cas, NamedEntity.class); @@ -242,7 +243,7 @@ public void thatPredictionWithPretrainigWorks() throws Exception sut.train(context, emptyList()); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); List predictions = getPredictions(cas, NamedEntity.class); @@ -275,7 +276,7 @@ public void thatPredictionWithPretrainigWorks_CaseInsensitve() throws Exception sut.train(context, emptyList()); - sut.predict(context, cas); + sut.predict(new PredictionContext(context), cas); List predictions = getPredictions(cas, NamedEntity.class); diff --git a/inception/inception-imls-weblicht/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/weblicht/WeblichtRecommender.java b/inception/inception-imls-weblicht/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/weblicht/WeblichtRecommender.java index 4c3d11a6b89..95550f9d6ab 100644 --- a/inception/inception-imls-weblicht/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/weblicht/WeblichtRecommender.java +++ b/inception/inception-imls-weblicht/src/main/java/de/tudarmstadt/ukp/inception/recommendation/imls/weblicht/WeblichtRecommender.java @@ -53,6 +53,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.DataSplitter; import de.tudarmstadt.ukp.inception.recommendation.api.evaluation.EvaluationResult; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -104,7 +105,7 @@ public void train(RecommenderContext aContext, List aCasses) } @Override - public Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException { // Begin and end are not used here because we do not know what kind of WebLicht pipeline diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/PredictionContext.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/PredictionContext.java new file mode 100644 index 00000000000..2020df9a604 --- /dev/null +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/PredictionContext.java @@ -0,0 +1,107 @@ +/* + * 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.recommender; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import de.tudarmstadt.ukp.clarin.webanno.security.model.User; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext.Key; +import de.tudarmstadt.ukp.inception.support.logging.LogMessage; + +public class PredictionContext +{ + private RecommenderContext modelContext; + private List messages; + private boolean closed = false; + private Optional user; + + public PredictionContext(RecommenderContext aCtx) + { + modelContext = aCtx; + messages = new ArrayList<>(); + } + + synchronized public Optional get(Key aKey) + { + return modelContext.get(aKey); + } + + synchronized public void info(String aFormat, Object... aValues) + { + if (closed) { + throw new IllegalStateException("Adding data to a closed context is not permitted."); + } + + messages.add(LogMessage.info(this, aFormat, aValues)); + } + + synchronized public void warn(String aFormat, Object... aValues) + { + if (closed) { + throw new IllegalStateException("Adding data to a closed context is not permitted."); + } + + messages.add(LogMessage.warn(this, aFormat, aValues)); + } + + synchronized public void error(String aFormat, Object... aValues) + { + if (closed) { + throw new IllegalStateException("Adding data to a closed context is not permitted."); + } + + messages.add(LogMessage.error(this, aFormat, aValues)); + } + + public List getMessages() + { + return messages; + } + + /** + * Close the context. Further modifications to the context are not permitted. + */ + synchronized public void close() + { + if (!closed) { + closed = true; + messages = Collections.unmodifiableList(messages); + } + } + + /** + * @return whether the context is closed. + */ + synchronized public boolean isClosed() + { + return closed; + } + + public Optional getUser() + { + return user; + } + + public void setUser(User aUser) + { + user = Optional.ofNullable(aUser); + } +} diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommendationEngine.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommendationEngine.java index d2494c4362a..2fa08e2934b 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommendationEngine.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommendationEngine.java @@ -84,7 +84,7 @@ public abstract void train(RecommenderContext aContext, List aCasses) * @throws RecommendationException * if there was a problem during prediction */ - public void predict(RecommenderContext aContext, CAS aCas) throws RecommendationException + public void predict(PredictionContext aContext, CAS aCas) throws RecommendationException { predict(aContext, aCas, 0, aCas.getDocumentText().length()); } @@ -111,7 +111,7 @@ public void predict(RecommenderContext aContext, CAS aCas) throws Recommendation * @throws RecommendationException * if there was a problem during prediction */ - public abstract Range predict(RecommenderContext aContext, CAS aCas, int aBegin, int aEnd) + public abstract Range predict(PredictionContext aContext, CAS aCas, int aBegin, int aEnd) throws RecommendationException; /** diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommenderContext.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommenderContext.java index 115729ed561..db159e58c5e 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommenderContext.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/RecommenderContext.java @@ -24,8 +24,6 @@ import java.util.Map; import java.util.Optional; -import javax.annotation.Nullable; - import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.inception.support.logging.LogMessage; @@ -43,7 +41,6 @@ public RecommenderContext() } @SuppressWarnings("unchecked") - @Nullable synchronized public Optional get(Key aKey) { return Optional.ofNullable((T) store.get(aKey.name)); diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java index 7cb39bb9214..49e73ab531e 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java @@ -126,6 +126,7 @@ 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; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationException; @@ -1451,9 +1452,6 @@ private void computePredictions(LazyCas aOriginalCas, return; } - RecommenderContext ctx = context.get(); - ctx.setUser(aSessionOwner); - Optional> maybeFactory = getRecommenderFactory(recommender); if (maybeFactory.isEmpty()) { @@ -1485,7 +1483,7 @@ private void computePredictions(LazyCas aOriginalCas, try { RecommendationEngine engine = factory.build(recommender); - if (!engine.isReadyForPrediction(ctx)) { + if (!engine.isReadyForPrediction(context.get())) { aPredictions.log(LogMessage.info(recommender.getName(), "Recommender context is not ready... skipping")); LOG.info("Recommender context {} for user {} in project {} is not ready for " // @@ -1514,8 +1512,11 @@ private void computePredictions(LazyCas aOriginalCas, engine.getRecommender(), activePredictions, aDocument, aSessionOwner); } else { + var ctx = new PredictionContext(context.get()); + ctx.setUser(aSessionOwner); generateSuggestions(aPredictions, ctx, engine, activePredictions, aDocument, originalCas, predictionCas, predictionBegin, predictionEnd); + ctx.getMessages().forEach(aPredictions::log); } } // Catching Throwable is intentional here as we want to continue the @@ -1716,7 +1717,7 @@ private void inheritSuggestionsAtDocumentLevel(Project aProject, SourceDocument /** * Invokes the engine to produce new suggestions. */ - void generateSuggestions(Predictions aIncomingPredictions, RecommenderContext aCtx, + void generateSuggestions(Predictions aIncomingPredictions, PredictionContext aCtx, RecommendationEngine aEngine, Predictions aActivePredictions, SourceDocument aDocument, CAS aOriginalCas, CAS aPredictionCas, int aPredictionBegin, int aPredictionEnd) throws RecommendationException @@ -1729,6 +1730,7 @@ void generateSuggestions(Predictions aIncomingPredictions, RecommenderContext aC "Generating predictions for layer [%s]...", recommender.getLayer().getUiName())); LOG.trace("{}[{}]: Generating predictions for layer [{}]", sessionOwner, recommender.getName(), recommender.getLayer().getUiName()); + var predictedRange = aEngine.predict(aCtx, aPredictionCas, aPredictionBegin, aPredictionEnd); From 4105f2258f3e568d31d1dd8a4f4b118fa3a02987 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Sun, 24 Dec 2023 19:36:43 +0100 Subject: [PATCH 05/13] #2696 - Document-level recommendations - Move creation of renderers into new suggestion supports --- .../sidebar/MetadataSuggestionSupport.java | 8 +++ .../api}/RecommendationTypeRenderer.java | 12 +++- .../recommendation/api/SuggestionSupport.java | 3 + .../recommendation/api/model/Predictions.java | 14 +++++ .../api/model/SuggestionDocumentGroup.java | 7 ++- .../RecommenderServiceAutoConfiguration.java | 38 +++--------- .../RecommendationRelationRenderer.java | 44 ++++--------- .../render/RecommendationRenderer.java | 61 +++++++++++-------- .../render/RecommendationSpanRenderer.java | 42 +++++-------- .../service/RelationSuggestionSupport.java | 16 ++++- .../service/SpanSuggestionSupport.java | 20 +++++- ...ommendationServiceImplIntegrationTest.java | 4 +- ...onSuggestionVisibilityCalculationTest.java | 2 +- ...anSuggestionVisibilityCalculationTest.java | 2 +- 14 files changed, 151 insertions(+), 122 deletions(-) rename inception/{inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render => inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api}/RecommendationTypeRenderer.java (73%) diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java index 54e2830593b..33e7969c561 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java @@ -26,6 +26,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.Optional; import org.apache.commons.lang3.NotImplementedException; import org.apache.uima.cas.AnnotationBaseFS; @@ -41,6 +42,7 @@ 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.RecommendationTypeRenderer; import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport_ImplBase; import de.tudarmstadt.ukp.inception.recommendation.api.event.RecommendationRejectedEvent; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; @@ -269,4 +271,10 @@ var record = new LearningRecord(); record.setAnnotationFeature(aFeature); return record; } + + @Override + public Optional> getRenderer() + { + return Optional.empty(); + } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationTypeRenderer.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationTypeRenderer.java similarity index 73% rename from inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationTypeRenderer.java rename to inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationTypeRenderer.java index 0d4e51fa33c..9262485bec9 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationTypeRenderer.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationTypeRenderer.java @@ -15,9 +15,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.render; +package de.tudarmstadt.ukp.inception.recommendation.api; -import de.tudarmstadt.ukp.inception.recommendation.api.model.Predictions; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; @@ -35,8 +37,12 @@ public interface RecommendationTypeRenderer * * @param aVdoc * a {@link VDocument} containing annotations for the given layer + * @param aSuggestions + * the suggestions to render * @param aRequest * a render request */ - void render(VDocument aVdoc, RenderRequest aRequest, Predictions aPredictions, T aAdapter); + void render(VDocument aVdoc, RenderRequest aRequest, + SuggestionDocumentGroup aSuggestions, + AnnotationLayer aLayer); } 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 2a5aad69f2c..6c692261d10 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 @@ -18,6 +18,7 @@ package de.tudarmstadt.ukp.inception.recommendation.api; import java.util.Collection; +import java.util.Optional; import org.apache.uima.cas.AnnotationBaseFS; import org.apache.uima.cas.CAS; @@ -58,4 +59,6 @@ void calculateSuggestionVisibility(String aSess LearningRecord toLearningRecord(SourceDocument aDocument, String aUsername, AnnotationSuggestion aSuggestion, AnnotationFeature aFeature, LearningRecordUserAction aUserAction, LearningRecordChangeLocation aLocation); + + Optional> getRenderer(); } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java index c725cb3a795..14f0c17e38a 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java @@ -345,6 +345,20 @@ public List getPredictionsByDocument(String aDocumentName) } } + public List getPredictionsByDocument(String aDocumentName, + int aWindowBegin, int aWindowEnd) + { + synchronized (predictionsLock) { + var byDocument = idxDocuments.getOrDefault(aDocumentName, emptyMap()); + return byDocument.entrySet().stream() // + .filter(f -> AnnotationPredicates.overlapping(f.getValue().getWindowBegin(), + f.getValue().getWindowEnd(), aWindowBegin, aWindowEnd)) + .sorted(comparingInt(e2 -> e2.getValue().getWindowBegin())) // + .map(Map.Entry::getValue) // + .collect(toList()); + } + } + public void markDocumentAsPredictionCompleted(SourceDocument aDocument) { synchronized (seenDocumentsForPrediction) { diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java index 012305260f0..c79d7fc5ba0 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java @@ -22,6 +22,7 @@ import java.util.AbstractCollection; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -76,9 +77,13 @@ public static SuggestionDocumentGroup groups * the list to retrieve suggestions from * @return a SuggestionDocumentGroup where only suggestions of type V are added */ - public static Map, SuggestionDocumentGroup> groupByType( + public static Map, SuggestionDocumentGroup> groupByType( List aSuggestions) { + if (aSuggestions == null || aSuggestions.isEmpty()) { + return Collections.emptyMap(); + } + var groups = new LinkedHashMap, List>(); for (var suggestion : aSuggestions) { 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 17154b0137c..430b9443461 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 @@ -54,9 +54,7 @@ import de.tudarmstadt.ukp.inception.recommendation.metrics.RecommendationMetricsImpl; import de.tudarmstadt.ukp.inception.recommendation.project.ProjectRecommendersMenuItem; import de.tudarmstadt.ukp.inception.recommendation.project.RecommenderProjectSettingsPanelFactory; -import de.tudarmstadt.ukp.inception.recommendation.render.RecommendationRelationRenderer; import de.tudarmstadt.ukp.inception.recommendation.render.RecommendationRenderer; -import de.tudarmstadt.ukp.inception.recommendation.render.RecommendationSpanRenderer; import de.tudarmstadt.ukp.inception.recommendation.service.LayerRecommendtionSupportRegistryImpl; import de.tudarmstadt.ukp.inception.recommendation.service.RecommendationServiceImpl; import de.tudarmstadt.ukp.inception.recommendation.service.RecommenderFactoryRegistryImpl; @@ -192,31 +190,11 @@ public RecommendationEventFooterItem recommendationEventFooterItem() @Bean public RecommendationRenderer recommendationRenderer(AnnotationSchemaService aAnnotationService, - RecommendationSpanRenderer aRecommendationSpanRenderer, - RecommendationRelationRenderer aRecommendationRelationRenderer, - RecommendationService aRecommendationService) - { - return new RecommendationRenderer(aAnnotationService, aRecommendationSpanRenderer, - aRecommendationRelationRenderer, aRecommendationService); - } - - @Bean - public RecommendationSpanRenderer recommendationSpanRenderer( - RecommendationService aRecommendationService, - AnnotationSchemaService aAnnotationService, FeatureSupportRegistry aFsRegistry, - RecommenderProperties aRecommenderProperties) - { - return new RecommendationSpanRenderer(aRecommendationService, aAnnotationService, - aFsRegistry, aRecommenderProperties); - } - - @Bean - public RecommendationRelationRenderer recommendationRelationRenderer( RecommendationService aRecommendationService, - AnnotationSchemaService aAnnotationService, FeatureSupportRegistry aFsRegistry) + SuggestionSupportRegistry aSuggestionSupportRegistry) { - return new RecommendationRelationRenderer(aRecommendationService, aAnnotationService, - aFsRegistry); + return new RecommendationRenderer(aAnnotationService, aRecommendationService, + aSuggestionSupportRegistry); } @Bean @@ -231,10 +209,12 @@ public SpanSuggestionSupport spanRecommendationSupport( RecommendationService aRecommendationService, LearningRecordService aLearningRecordService, ApplicationEventPublisher aApplicationEventPublisher, - AnnotationSchemaService aSchemaService) + AnnotationSchemaService aSchemaService, FeatureSupportRegistry aFeatureSupportRegistry, + RecommenderProperties aRecommenderProperties) { return new SpanSuggestionSupport(aRecommendationService, aLearningRecordService, - aApplicationEventPublisher, aSchemaService); + aApplicationEventPublisher, aSchemaService, aFeatureSupportRegistry, + aRecommenderProperties); } @Bean @@ -242,10 +222,10 @@ public RelationSuggestionSupport relationRecommendationSupport( RecommendationService aRecommendationService, LearningRecordService aLearningRecordService, ApplicationEventPublisher aApplicationEventPublisher, - AnnotationSchemaService aSchemaService) + AnnotationSchemaService aSchemaService, FeatureSupportRegistry aFeatureSupportRegistry) { return new RelationSuggestionSupport(aRecommendationService, aLearningRecordService, - aApplicationEventPublisher, aSchemaService); + aApplicationEventPublisher, aSchemaService, aFeatureSupportRegistry); } @Bean diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java index 63a329e13a9..9220ff51925 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java @@ -17,8 +17,6 @@ */ package de.tudarmstadt.ukp.inception.recommendation.render; -import static de.tudarmstadt.ukp.inception.annotation.storage.CasMetadataUtils.getSourceDocumentName; -import static de.tudarmstadt.ukp.inception.support.uima.WebAnnoCasUtil.getDocumentTitle; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; import static org.apache.uima.fit.util.CasUtil.selectAt; @@ -28,11 +26,13 @@ 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.annotation.layer.relation.RelationAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; -import de.tudarmstadt.ukp.inception.recommendation.api.model.Predictions; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationTypeRenderer; +import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.RelationSuggestion; -import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; +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; @@ -41,12 +41,6 @@ import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupport; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; -/** - *

- * This class is exposed as a Spring Component via - * {@link RecommenderServiceAutoConfiguration#recommendationRelationRenderer}. - *

- */ public class RecommendationRelationRenderer implements RecommendationTypeRenderer { @@ -62,27 +56,15 @@ public RecommendationRelationRenderer(RecommendationService aRecommendationServi fsRegistry = aFsRegistry; } - /** - * Add annotations from the CAS, which is controlled by the window size, to the VDocument - * {@link VDocument} - * - * @param aVDoc - * A VDocument containing annotations for the given layer - * @param aPredictions - * the predictions to render - */ @Override - public void render(VDocument aVDoc, RenderRequest aRequest, Predictions aPredictions, - RelationAdapter aTypeAdapter) + public void render(VDocument aVDoc, RenderRequest aRequest, + SuggestionDocumentGroup aSuggestions, + AnnotationLayer aLayer) { var cas = aRequest.getCas(); - var layer = aTypeAdapter.getLayer(); // TODO #176 use the document Id once it it available in the CAS - var sourceDocumentName = getSourceDocumentName(cas).orElseGet(() -> getDocumentTitle(cas)); - var groupedPredictions = aPredictions.getGroupedPredictions(RelationSuggestion.class, - sourceDocumentName, layer, aRequest.getWindowBeginOffset(), - aRequest.getWindowEndOffset()); + var groupedPredictions = (SuggestionDocumentGroup) aSuggestions; // No recommendations to render for this layer if (groupedPredictions.isEmpty()) { @@ -91,16 +73,16 @@ public void render(VDocument aVDoc, RenderRequest aRequest, Predictions aPredict recommendationService.calculateSuggestionVisibility( aRequest.getSessionOwner().getUsername(), aRequest.getSourceDocument(), cas, - aRequest.getAnnotationUser().getUsername(), layer, groupedPredictions, + aRequest.getAnnotationUser().getUsername(), aLayer, groupedPredictions, aRequest.getWindowBeginOffset(), aRequest.getWindowEndOffset()); var pref = recommendationService.getPreferences(aRequest.getAnnotationUser(), - layer.getProject()); + aLayer.getProject()); - var attachType = CasUtil.getType(cas, layer.getAttachType().getName()); + 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(layer).stream() + var features = annotationService.listSupportedFeatures(aLayer).stream() .collect(toMap(AnnotationFeature::getName, identity())); for (var group : groupedPredictions) { @@ -136,7 +118,7 @@ public void render(VDocument aVDoc, RenderRequest aRequest, Predictions aPredict ? Map.of(suggestion.getFeature(), annotation) : Map.of(); - var arc = new VArc(layer, suggestion.getVID(), VID.of(source), VID.of(target), + var arc = new VArc(aLayer, suggestion.getVID(), VID.of(source), VID.of(target), "\uD83E\uDD16 " + suggestion.getUiLabel(), featureAnnotation, COLOR); arc.setScore(suggestion.getScore()); diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java index 1dbbe83c1f4..f8756a5ddab 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java @@ -18,15 +18,14 @@ package de.tudarmstadt.ukp.inception.recommendation.render; import static de.tudarmstadt.ukp.clarin.webanno.model.Mode.ANNOTATION; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.CHAIN_TYPE; +import static de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup.groupByType; +import static java.util.stream.Collectors.groupingBy; import org.springframework.core.annotation.Order; -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.relation.RelationAdapter; -import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupportRegistry; +import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.pipeline.RenderStep; @@ -47,19 +46,16 @@ public class RecommendationRenderer public static final String ID = "RecommendationRenderer"; private final AnnotationSchemaService annotationService; - private final RecommendationSpanRenderer recommendationSpanRenderer; - private final RecommendationRelationRenderer recommendationRelationRenderer; private final RecommendationService recommendationService; + private final SuggestionSupportRegistry suggestionSupportRegistry; public RecommendationRenderer(AnnotationSchemaService aAnnotationService, - RecommendationSpanRenderer aRecommendationSpanRenderer, - RecommendationRelationRenderer aRecommendationRelationRenderer, - RecommendationService aRecommendationService) + RecommendationService aRecommendationService, + SuggestionSupportRegistry aSuggestionSupportRegistry) { annotationService = aAnnotationService; - recommendationSpanRenderer = aRecommendationSpanRenderer; - recommendationRelationRenderer = aRecommendationRelationRenderer; recommendationService = aRecommendationService; + suggestionSupportRegistry = aSuggestionSupportRegistry; } @Override @@ -100,24 +96,41 @@ public void render(VDocument aVDoc, RenderRequest aRequest) return; } - // Add the suggestions to the visual document + var suggestions = predictions.getPredictionsByDocument( + aRequest.getSourceDocument().getName(), aRequest.getWindowBeginOffset(), + aRequest.getWindowEndOffset()); + var suggestionsByLayer = suggestions.stream() + .collect(groupingBy(AnnotationSuggestion::getLayerId)); + for (var layer : aRequest.getVisibleLayers()) { - if (Token.class.getName().equals(layer.getName()) - || Sentence.class.getName().equals(layer.getName()) - || CHAIN_TYPE.equals(layer.getType()) - || !layer.isEnabled()) { /* Hide layer if not enabled */ + if (!layer.isEnabled() || layer.isReadonly()) { continue; } - var adapter = annotationService.getAdapter(layer); - if (adapter instanceof SpanAdapter) { - recommendationSpanRenderer.render(aVDoc, aRequest, predictions, - (SpanAdapter) adapter); + var suggestionsByType = groupByType(suggestionsByLayer.get(layer.getId())); + if (suggestionsByType.isEmpty()) { + continue; } - if (adapter instanceof RelationAdapter) { - recommendationRelationRenderer.render(aVDoc, aRequest, predictions, - (RelationAdapter) adapter); + for (var suggestionGroup : suggestionsByType.entrySet()) { + var maybeSuggestion = suggestionGroup.getValue().stream() + .filter(group -> !group.isEmpty()) // + .flatMap(group -> group.stream()) // + .findAny(); + + if (!maybeSuggestion.isPresent()) { + continue; + } + + var maybeSupport = suggestionSupportRegistry + .findGenericExtension(maybeSuggestion.get()); + + if (!maybeSupport.isPresent()) { + continue; + } + + maybeSupport.get().getRenderer().ifPresent(renderer -> renderer.render(aVDoc, + aRequest, suggestionGroup.getValue(), layer)); } } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java index 264f3be32b4..62040f93a3e 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java @@ -23,12 +23,14 @@ import java.util.Map; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; -import de.tudarmstadt.ukp.inception.recommendation.api.model.Predictions; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationTypeRenderer; +import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderProperties; -import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; import de.tudarmstadt.ukp.inception.rendering.vmodel.VRange; @@ -36,12 +38,6 @@ import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; -/** - *

- * This class is exposed as a Spring Component via - * {@link RecommenderServiceAutoConfiguration#recommendationSpanRenderer}. - *

- */ public class RecommendationSpanRenderer implements RecommendationTypeRenderer { @@ -60,40 +56,30 @@ public RecommendationSpanRenderer(RecommendationService aRecommendationService, recommenderProperties = aRecommenderProperties; } - /** - * Add annotations from the CAS, which is controlled by the window size, to the VDocument - * {@link VDocument} - * - * @param vdoc - * A VDocument containing annotations for the given layer - * @param aPredictions - * the predictions to render - */ @Override - public void render(VDocument vdoc, RenderRequest aRequest, Predictions aPredictions, - SpanAdapter aTypeAdapter) + public void render(VDocument vdoc, RenderRequest aRequest, + SuggestionDocumentGroup aSuggestions, + AnnotationLayer aLayer) { - var cas = aRequest.getCas(); - var layer = aTypeAdapter.getLayer(); - var groups = aPredictions.getGroupedPredictions(SpanSuggestion.class, - aRequest.getSourceDocument().getName(), layer, aRequest.getWindowBeginOffset(), - aRequest.getWindowEndOffset()); + var groups = (SuggestionDocumentGroup) aSuggestions; // No recommendations to render for this layer if (groups.isEmpty()) { return; } + var cas = aRequest.getCas(); + recommendationService.calculateSuggestionVisibility( aRequest.getSessionOwner().getUsername(), aRequest.getSourceDocument(), cas, - aRequest.getAnnotationUser().getUsername(), layer, groups, + aRequest.getAnnotationUser().getUsername(), aLayer, groups, aRequest.getWindowBeginOffset(), aRequest.getWindowEndOffset()); var pref = recommendationService.getPreferences(aRequest.getAnnotationUser(), - layer.getProject()); + aLayer.getProject()); // Bulk-load all the features of this layer to avoid having to do repeated DB accesses later - var features = annotationService.listSupportedFeatures(layer).stream() + var features = annotationService.listSupportedFeatures(aLayer).stream() .collect(toMap(AnnotationFeature::getName, identity())); for (var suggestionGroup : groups) { @@ -114,7 +100,7 @@ public void render(VDocument vdoc, RenderRequest aRequest, Predictions aPredicti ? Map.of(suggestion.getFeature(), annotation) : Map.of(); - var v = new VSpan(layer, suggestion.getVID(), range.get(), featureAnnotation, + var v = new VSpan(aLayer, suggestion.getVID(), range.get(), featureAnnotation, COLOR); v.setScore(suggestion.getScore()); v.setActionButtons(recommenderProperties.isActionButtonsEnabled()); diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionSupport.java index 5b8239b2fc8..8a475d10920 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionSupport.java @@ -32,6 +32,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; import org.apache.uima.cas.CAS; @@ -49,6 +50,7 @@ import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationTypeRenderer; 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; @@ -58,9 +60,11 @@ 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.render.RecommendationRelationRenderer; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; 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; public class RelationSuggestionSupport extends SuggestionSupport_ImplBase @@ -69,13 +73,16 @@ public class RelationSuggestionSupport public static final String TYPE = "RELATION"; + private final FeatureSupportRegistry featureSupportRegistry; + public RelationSuggestionSupport(RecommendationService aRecommendationService, LearningRecordService aLearningRecordService, ApplicationEventPublisher aApplicationEventPublisher, - AnnotationSchemaService aSchemaService) + AnnotationSchemaService aSchemaService, FeatureSupportRegistry aFeatureSupportRegistry) { super(aRecommendationService, aLearningRecordService, aApplicationEventPublisher, aSchemaService); + featureSupportRegistry = aFeatureSupportRegistry; } @Override @@ -359,4 +366,11 @@ var record = new LearningRecord(); record.setAnnotationFeature(aFeature); return record; } + + @Override + public Optional> getRenderer() + { + return Optional.of(new RecommendationRelationRenderer(recommendationService, schemaService, + featureSupportRegistry)); + } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionSupport.java index 9a4da06ee85..0f773b3835a 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionSupport.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.TreeMap; import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; @@ -56,6 +57,7 @@ import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationTypeRenderer; import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport_ImplBase; import de.tudarmstadt.ukp.inception.recommendation.api.event.RecommendationRejectedEvent; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; @@ -65,10 +67,13 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.Offset; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; +import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderProperties; +import de.tudarmstadt.ukp.inception.recommendation.render.RecommendationSpanRenderer; import de.tudarmstadt.ukp.inception.recommendation.util.OverlapIterator; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; 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; public class SpanSuggestionSupport extends SuggestionSupport_ImplBase @@ -77,13 +82,19 @@ public class SpanSuggestionSupport public static final String TYPE = "SPAN"; + private final FeatureSupportRegistry featureSupportRegistry; + private final RecommenderProperties recommenderProperties; + public SpanSuggestionSupport(RecommendationService aRecommendationService, LearningRecordService aLearningRecordService, ApplicationEventPublisher aApplicationEventPublisher, - AnnotationSchemaService aSchemaService) + AnnotationSchemaService aSchemaService, FeatureSupportRegistry aFeatureSupportRegistry, + RecommenderProperties aRecommenderProperties) { super(aRecommendationService, aLearningRecordService, aApplicationEventPublisher, aSchemaService); + featureSupportRegistry = aFeatureSupportRegistry; + recommenderProperties = aRecommenderProperties; } @Override @@ -432,4 +443,11 @@ var record = new LearningRecord(); record.setAnnotationFeature(aFeature); return record; } + + @Override + public Optional> getRenderer() + { + return Optional.of(new RecommendationSpanRenderer(recommendationService, schemaService, + featureSupportRegistry, recommenderProperties)); + } } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java index 0e366ad98c2..85f1bf9c917 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java @@ -118,8 +118,8 @@ public class RecommendationServiceImplIntegrationTest public void setUp() throws Exception { layerRecommendtionSupportRegistry = new LayerRecommendtionSupportRegistryImpl(asList( // - new SpanSuggestionSupport(sut, sut, null, schemaService), - new RelationSuggestionSupport(sut, sut, null, schemaService))); + new SpanSuggestionSupport(sut, sut, null, schemaService, null, null), + new RelationSuggestionSupport(sut, sut, null, schemaService, null))); layerRecommendtionSupportRegistry.init(); sut = new RecommendationServiceImpl(null, null, null, recommenderFactoryRegistry, null, diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionVisibilityCalculationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionVisibilityCalculationTest.java index 4e104edcf98..0ea4e2ec346 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionVisibilityCalculationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionVisibilityCalculationTest.java @@ -76,7 +76,7 @@ public void setUp() throws Exception when(annoService.listSupportedFeatures(layer)).thenReturn(asList(feature)); - sut = new RelationSuggestionSupport(null, learningRecordService, null, annoService); + sut = new RelationSuggestionSupport(null, learningRecordService, null, annoService, null); } @Test diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionVisibilityCalculationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionVisibilityCalculationTest.java index 0bfb80cddcb..3449ec30c15 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionVisibilityCalculationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionVisibilityCalculationTest.java @@ -94,7 +94,7 @@ public void setUp() throws Exception lenient().when(annoService.listSupportedFeatures(layer)).thenReturn(asList(feature)); lenient().when(annoService.listSupportedFeatures(layer2)).thenReturn(asList(feature2)); - sut = new SpanSuggestionSupport(null, learningRecordService, null, annoService); + sut = new SpanSuggestionSupport(null, learningRecordService, null, annoService, null, null); } @Test From 39d22eb02d8793908b86083abff6722ccc238ea9 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Sun, 24 Dec 2023 20:05:32 +0100 Subject: [PATCH 06/13] #2696 - Document-level recommendations - Move classes to other packages --- .../docanno/sidebar/MetadataSuggestionSupport.java | 4 ++-- ...ationTypeRenderer.java => SuggestionRenderer.java} | 3 +-- .../recommendation/api/SuggestionSupport.java | 2 +- .../config/RecommenderServiceAutoConfiguration.java | 4 ++-- .../RelationSuggestionRenderer.java} | 11 +++++------ .../RelationSuggestionSupport.java | 9 ++++----- .../recommendation/service/SuggestionExtraction.java | 4 ++-- .../SpanSuggestionRenderer.java} | 11 +++++------ .../{service => span}/SpanSuggestionSupport.java | 9 ++++----- .../RelationSuggestionVisibilityCalculationTest.java | 3 ++- .../inception/recommendation/service/Fixtures.java | 10 +++++----- .../RecommendationServiceImplIntegrationTest.java | 4 ++++ .../SpanSuggestionExtractionTest.java | 3 ++- .../SpanSuggestionVisibilityCalculationTest.java | 5 +++-- 14 files changed, 42 insertions(+), 40 deletions(-) rename inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/{RecommendationTypeRenderer.java => SuggestionRenderer.java} (93%) rename inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/{render/RecommendationRelationRenderer.java => relation/RelationSuggestionRenderer.java} (93%) rename inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/{service => relation}/RelationSuggestionSupport.java (97%) rename inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/{render/RecommendationSpanRenderer.java => span/SpanSuggestionRenderer.java} (93%) rename inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/{service => span}/SpanSuggestionSupport.java (98%) rename inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/{service => relation}/RelationSuggestionVisibilityCalculationTest.java (97%) rename inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/{service => span}/SpanSuggestionExtractionTest.java (96%) rename inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/{service => span}/SpanSuggestionVisibilityCalculationTest.java (98%) diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java index 33e7969c561..a7a313fa2ca 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java @@ -42,7 +42,7 @@ 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.RecommendationTypeRenderer; +import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionRenderer; import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport_ImplBase; import de.tudarmstadt.ukp.inception.recommendation.api.event.RecommendationRejectedEvent; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; @@ -273,7 +273,7 @@ var record = new LearningRecord(); } @Override - public Optional> getRenderer() + public Optional getRenderer() { return Optional.empty(); } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationTypeRenderer.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionRenderer.java similarity index 93% rename from inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationTypeRenderer.java rename to inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionRenderer.java index 9262485bec9..2609c44dbb5 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationTypeRenderer.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionRenderer.java @@ -22,12 +22,11 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; import de.tudarmstadt.ukp.inception.rendering.request.RenderRequest; import de.tudarmstadt.ukp.inception.rendering.vmodel.VDocument; -import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; /** * Type Adapters for span, arc, and chain annotations */ -public interface RecommendationTypeRenderer +public interface SuggestionRenderer { String COLOR = "#cccccc"; 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 6c692261d10..d5e79d31d31 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 @@ -60,5 +60,5 @@ LearningRecord toLearningRecord(SourceDocument aDocument, String aUsername, AnnotationSuggestion aSuggestion, AnnotationFeature aFeature, LearningRecordUserAction aUserAction, LearningRecordChangeLocation aLocation); - Optional> getRenderer(); + Optional getRenderer(); } 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 430b9443461..e5054270683 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 @@ -54,13 +54,13 @@ import de.tudarmstadt.ukp.inception.recommendation.metrics.RecommendationMetricsImpl; import de.tudarmstadt.ukp.inception.recommendation.project.ProjectRecommendersMenuItem; import de.tudarmstadt.ukp.inception.recommendation.project.RecommenderProjectSettingsPanelFactory; +import de.tudarmstadt.ukp.inception.recommendation.relation.RelationSuggestionSupport; import de.tudarmstadt.ukp.inception.recommendation.render.RecommendationRenderer; import de.tudarmstadt.ukp.inception.recommendation.service.LayerRecommendtionSupportRegistryImpl; import de.tudarmstadt.ukp.inception.recommendation.service.RecommendationServiceImpl; import de.tudarmstadt.ukp.inception.recommendation.service.RecommenderFactoryRegistryImpl; -import de.tudarmstadt.ukp.inception.recommendation.service.RelationSuggestionSupport; -import de.tudarmstadt.ukp.inception.recommendation.service.SpanSuggestionSupport; import de.tudarmstadt.ukp.inception.recommendation.sidebar.RecommendationSidebarFactory; +import de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport; import de.tudarmstadt.ukp.inception.scheduling.SchedulingService; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionRenderer.java similarity index 93% rename from inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java rename to inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionRenderer.java index 2013376531b..9ac92151a12 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRelationRenderer.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionRenderer.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.render; +package de.tudarmstadt.ukp.inception.recommendation.relation; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; @@ -28,9 +28,8 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; -import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationTypeRenderer; +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; @@ -42,14 +41,14 @@ import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupport; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; -public class RecommendationRelationRenderer - implements RecommendationTypeRenderer +public class RelationSuggestionRenderer + implements SuggestionRenderer { private final RecommendationService recommendationService; private final AnnotationSchemaService annotationService; private final FeatureSupportRegistry fsRegistry; - public RecommendationRelationRenderer(RecommendationService aRecommendationService, + public RelationSuggestionRenderer(RecommendationService aRecommendationService, AnnotationSchemaService aAnnotationService, FeatureSupportRegistry aFsRegistry) { recommendationService = aRecommendationService; diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionSupport.java similarity index 97% rename from inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionSupport.java rename to inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionSupport.java index 8a475d10920..826bdd554da 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionSupport.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.service; +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.recommendation.api.model.AnnotationSuggestion.FLAG_SKIPPED; @@ -50,7 +50,7 @@ import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; -import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationTypeRenderer; +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; @@ -60,7 +60,6 @@ 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.render.RecommendationRelationRenderer; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; @@ -368,9 +367,9 @@ var record = new LearningRecord(); } @Override - public Optional> getRenderer() + public Optional getRenderer() { - return Optional.of(new RecommendationRelationRenderer(recommendationService, schemaService, + return Optional.of(new RelationSuggestionRenderer(recommendationService, schemaService, featureSupportRegistry)); } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java index d71f4903997..c75b08df666 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java @@ -251,7 +251,7 @@ static void extractSpanSuggestion(ExtractionContext ctx, TOP predictedFS) } } - static List extractSuggestions(int aGeneration, CAS aOriginalCas, + public static List extractSuggestions(int aGeneration, CAS aOriginalCas, CAS aPredictionCas, SourceDocument aDocument, Recommender aRecommender) { var ctx = new ExtractionContext(aGeneration, aRecommender, aDocument, aOriginalCas, @@ -378,7 +378,7 @@ private static Optional getOffsetsAnchoredOnSingleTokens(CAS aOriginalCa return Optional.of(new Offset(begin, end)); } - static Optional getOffsetsAnchoredOnTokens(CAS aOriginalCas, + public static Optional getOffsetsAnchoredOnTokens(CAS aOriginalCas, Annotation aPredictedAnnotation) { var tokens = aOriginalCas.select(Token.class) // diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionRenderer.java similarity index 93% rename from inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java rename to inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionRenderer.java index c557294819a..ee5a066ed54 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationSpanRenderer.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionRenderer.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.render; +package de.tudarmstadt.ukp.inception.recommendation.span; import static java.util.function.Function.identity; import static java.util.stream.Collectors.toMap; @@ -25,9 +25,8 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; -import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; -import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationTypeRenderer; +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.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; @@ -39,15 +38,15 @@ import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; -public class RecommendationSpanRenderer - implements RecommendationTypeRenderer +public class SpanSuggestionRenderer + implements SuggestionRenderer { private final RecommendationService recommendationService; private final AnnotationSchemaService annotationService; private final FeatureSupportRegistry fsRegistry; private final RecommenderProperties recommenderProperties; - public RecommendationSpanRenderer(RecommendationService aRecommendationService, + public SpanSuggestionRenderer(RecommendationService aRecommendationService, AnnotationSchemaService aAnnotationService, FeatureSupportRegistry aFsRegistry, RecommenderProperties aRecommenderProperties) { diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java similarity index 98% rename from inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionSupport.java rename to inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java index 0f773b3835a..7d3679a3c5d 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.service; +package de.tudarmstadt.ukp.inception.recommendation.span; import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_OVERLAP; import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_SKIPPED; @@ -57,7 +57,7 @@ import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; -import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationTypeRenderer; +import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionRenderer; import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport_ImplBase; import de.tudarmstadt.ukp.inception.recommendation.api.event.RecommendationRejectedEvent; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; @@ -68,7 +68,6 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderProperties; -import de.tudarmstadt.ukp.inception.recommendation.render.RecommendationSpanRenderer; import de.tudarmstadt.ukp.inception.recommendation.util.OverlapIterator; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; @@ -445,9 +444,9 @@ var record = new LearningRecord(); } @Override - public Optional> getRenderer() + public Optional getRenderer() { - return Optional.of(new RecommendationSpanRenderer(recommendationService, schemaService, + return Optional.of(new SpanSuggestionRenderer(recommendationService, schemaService, featureSupportRegistry, recommenderProperties)); } } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionVisibilityCalculationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionVisibilityCalculationTest.java similarity index 97% rename from inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionVisibilityCalculationTest.java rename to inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionVisibilityCalculationTest.java index 0ea4e2ec346..103889e24d2 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RelationSuggestionVisibilityCalculationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/relation/RelationSuggestionVisibilityCalculationTest.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.service; +package de.tudarmstadt.ukp.inception.recommendation.relation; import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.getInvisibleSuggestions; import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.getVisibleSuggestions; @@ -45,6 +45,7 @@ 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.relation.RelationSuggestionSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @ExtendWith(MockitoExtension.class) 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 4e7d92aaf77..56201f6b50a 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 @@ -43,7 +43,7 @@ public class Fixtures private final static String CONFIDENCE_EXPLANATION = "Predictor A: 0.05 | Predictor B: 0.15"; private final static String COVERED_TEXT = "TestText"; - static List getInvisibleSuggestions( + public static List getInvisibleSuggestions( Collection> aSuggestions) { return aSuggestions.stream() // @@ -52,7 +52,7 @@ static List getInvisibleSuggestions( .collect(toList()); } - static List getVisibleSuggestions( + public static List getVisibleSuggestions( Collection> aSuggestions) { return aSuggestions.stream() // @@ -61,8 +61,8 @@ static List getVisibleSuggestions( .collect(toList()); } - static SuggestionDocumentGroup makeSpanSuggestionGroup(SourceDocument doc, - AnnotationFeature aFeat, int[][] vals) + public static SuggestionDocumentGroup makeSpanSuggestionGroup( + SourceDocument doc, AnnotationFeature aFeat, int[][] vals) { List suggestions = new ArrayList<>(); for (int[] val : vals) { @@ -74,7 +74,7 @@ static SuggestionDocumentGroup makeSpanSuggestionGroup(SourceDoc return new SuggestionDocumentGroup<>(suggestions); } - static SuggestionDocumentGroup makeRelationSuggestionGroup( + public static SuggestionDocumentGroup makeRelationSuggestionGroup( SourceDocument doc, AnnotationFeature aFeat, int[][] vals) { List suggestions = new ArrayList<>(); diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java index 85f1bf9c917..12f0fea6867 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java @@ -81,6 +81,10 @@ 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.recommender.RecommendationEngineFactory; +import de.tudarmstadt.ukp.inception.recommendation.relation.RelationSuggestionSupport; +import de.tudarmstadt.ukp.inception.recommendation.service.LayerRecommendtionSupportRegistryImpl; +import de.tudarmstadt.ukp.inception.recommendation.service.RecommendationServiceImpl; +import de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport; import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistry; import de.tudarmstadt.ukp.inception.schema.service.AnnotationSchemaServiceImpl; import de.tudarmstadt.ukp.inception.schema.service.FeatureSupportRegistryImpl; diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionExtractionTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java similarity index 96% rename from inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionExtractionTest.java rename to inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java index d61e7aee8db..3716c9750a0 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionExtractionTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.service; +package de.tudarmstadt.ukp.inception.recommendation.span; import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; import static de.tudarmstadt.ukp.inception.support.uima.AnnotationBuilder.buildAnnotation; @@ -36,6 +36,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderTypeSystemUtils; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction; import de.tudarmstadt.ukp.inception.support.uima.SegmentationUtils; class SpanSuggestionExtractionTest diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionVisibilityCalculationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java similarity index 98% rename from inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionVisibilityCalculationTest.java rename to inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java index 3449ec30c15..674f0410f8b 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/SpanSuggestionVisibilityCalculationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.service; +package de.tudarmstadt.ukp.inception.recommendation.span; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction.REJECTED; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction.SKIPPED; @@ -23,7 +23,7 @@ import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.getVisibleSuggestions; import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.makeSpanSuggestionGroup; import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.makeSuggestion; -import static de.tudarmstadt.ukp.inception.recommendation.service.SpanSuggestionSupport.hideSuggestionsRejectedOrSkipped; +import static de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport.hideSuggestionsRejectedOrSkipped; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; import static org.apache.uima.cas.CAS.TYPE_NAME_STRING; @@ -55,6 +55,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.Offset; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; +import de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @ExtendWith(MockitoExtension.class) From 7bde88308eb22ea165403e73afd69e1ea65d5727 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Sun, 24 Dec 2023 20:24:05 +0100 Subject: [PATCH 07/13] #2696 - Document-level recommendations - Started moving extraction logic into the suggestion supports --- .../relation/RelationSuggestionSupport.java | 61 +++ .../service/ExtractionContext.java | 194 ++++++++++ .../service/SuggestionExtraction.java | 356 +----------------- .../span/SpanSuggestionSupport.java | 191 ++++++++++ ...ommendationServiceImplIntegrationTest.java | 35 -- .../RecommendationServiceImplTest.java | 112 ------ .../span/SpanSuggestionExtractionTest.java | 143 +++++++ 7 files changed, 608 insertions(+), 484 deletions(-) create mode 100644 inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/ExtractionContext.java 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 826bdd554da..475555ffe33 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 @@ -39,6 +39,8 @@ 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.cas.TOP; +import org.apache.uima.jcas.tcas.Annotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; @@ -60,7 +62,10 @@ 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.service.ExtractionContext; +import de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction; 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; @@ -372,4 +377,60 @@ public Optional getRenderer() return Optional.of(new RelationSuggestionRenderer(recommendationService, schemaService, featureSupportRegistry)); } + + public static void extractSuggestion(ExtractionContext ctx, TOP predictedFS) + { + var autoAcceptMode = SuggestionExtraction.getAutoAcceptMode(predictedFS, + ctx.getModeFeature()); + var labels = SuggestionExtraction.getPredictedLabels(predictedFS, ctx.getLabelFeature(), + ctx.isMultiLabels()); + var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); + var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); + + var source = (AnnotationFS) predictedFS.getFeatureValue(ctx.getSourceFeature()); + var target = (AnnotationFS) predictedFS.getFeatureValue(ctx.getTargetFeature()); + + var originalSource = findEquivalentSpan(ctx.getOriginalCas(), source); + var originalTarget = findEquivalentSpan(ctx.getOriginalCas(), target); + if (originalSource.isEmpty() || originalTarget.isEmpty()) { + LOG.debug("Unable to find source or target of predicted relation in original CAS"); + return; + } + + var position = new RelationPosition(originalSource.get(), originalTarget.get()); + + for (var label : labels) { + var suggestion = RelationSuggestion.builder() // + .withId(RelationSuggestion.NEW_ID) // + .withGeneration(ctx.getGeneration()) // + .withRecommender(ctx.getRecommender()) // + .withDocumentName(ctx.getDocument().getName()) // + .withPosition(position) // + .withLabel(label) // + .withUiLabel(label) // + .withScore(score) // + .withScoreExplanation(scoreExplanation) // + .withAutoAcceptMode(autoAcceptMode) // + .build(); + ctx.getResult().add(suggestion); + } + } + + /** + * 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(); + } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/ExtractionContext.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/ExtractionContext.java new file mode 100644 index 00000000000..8f8774585bf --- /dev/null +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/ExtractionContext.java @@ -0,0 +1,194 @@ +/* + * 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.service; + +import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX; +import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; +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 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.CAS.TYPE_NAME_STRING_ARRAY; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.uima.cas.CAS; +import org.apache.uima.cas.Feature; +import org.apache.uima.cas.Type; +import org.apache.uima.fit.util.CasUtil; + +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +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; + +public final class ExtractionContext +{ + private final int generation; + + private final SourceDocument document; + private final CAS originalCas; + private final CAS predictionCas; + private final String documentText; + + private final Recommender recommender; + + private final AnnotationLayer layer; + private final String typeName; + private final String featureName; + + private final Type predictedType; + + private final Feature labelFeature; + private final Feature sourceFeature; + private final Feature targetFeature; + private final Feature scoreFeature; + private final Feature scoreExplanationFeature; + private final Feature modeFeature; + private final Feature predictionFeature; + + private final boolean isMultiLabels; + + private final List result; + + public ExtractionContext(int aGeneration, Recommender aRecommender, SourceDocument aDocument, + CAS aOriginalCas, CAS aPredictionCas) + { + recommender = aRecommender; + + document = aDocument; + originalCas = aOriginalCas; + documentText = originalCas.getDocumentText(); + predictionCas = aPredictionCas; + + generation = aGeneration; + layer = aRecommender.getLayer(); + featureName = aRecommender.getFeature().getName(); + typeName = layer.getName(); + + predictedType = CasUtil.getType(aPredictionCas, typeName); + labelFeature = predictedType.getFeatureByBaseName(featureName); + sourceFeature = predictedType.getFeatureByBaseName(FEAT_REL_SOURCE); + targetFeature = predictedType.getFeatureByBaseName(FEAT_REL_TARGET); + scoreFeature = predictedType.getFeatureByBaseName(featureName + FEATURE_NAME_SCORE_SUFFIX); + scoreExplanationFeature = predictedType + .getFeatureByBaseName(featureName + FEATURE_NAME_SCORE_EXPLANATION_SUFFIX); + modeFeature = predictedType + .getFeatureByBaseName(featureName + FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX); + predictionFeature = predictedType.getFeatureByBaseName(FEATURE_NAME_IS_PREDICTION); + isMultiLabels = TYPE_NAME_STRING_ARRAY.equals(labelFeature.getRange().getName()); + + result = new ArrayList(); + } + + public int getGeneration() + { + return generation; + } + + public SourceDocument getDocument() + { + return document; + } + + public CAS getOriginalCas() + { + return originalCas; + } + + public CAS getPredictionCas() + { + return predictionCas; + } + + public String getDocumentText() + { + return documentText; + } + + public Recommender getRecommender() + { + return recommender; + } + + public AnnotationLayer getLayer() + { + return layer; + } + + public String getTypeName() + { + return typeName; + } + + public String getFeatureName() + { + return featureName; + } + + public Type getPredictedType() + { + return predictedType; + } + + public Feature getLabelFeature() + { + return labelFeature; + } + + public Feature getSourceFeature() + { + return sourceFeature; + } + + public Feature getTargetFeature() + { + return targetFeature; + } + + public Feature getScoreFeature() + { + return scoreFeature; + } + + public Feature getScoreExplanationFeature() + { + return scoreExplanationFeature; + } + + public Feature getModeFeature() + { + return modeFeature; + } + + public Feature getPredictionFeature() + { + return predictionFeature; + } + + public boolean isMultiLabels() + { + return isMultiLabels; + } + + public List getResult() + { + return result; + } +} \ No newline at end of file diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java index c75b08df666..2e189832c3f 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java @@ -17,51 +17,26 @@ */ package de.tudarmstadt.ukp.inception.recommendation.service; -import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX; -import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; -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 de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_SOURCE; -import static de.tudarmstadt.ukp.inception.support.WebAnnoConst.FEAT_REL_TARGET; -import static java.lang.Math.max; -import static java.lang.Math.min; -import static org.apache.uima.cas.CAS.TYPE_NAME_STRING_ARRAY; -import static org.apache.uima.fit.util.CasUtil.getType; - import java.lang.invoke.MethodHandles; -import java.util.ArrayList; import java.util.List; -import java.util.Optional; import org.apache.uima.cas.CAS; import org.apache.uima.cas.Feature; import org.apache.uima.cas.FeatureStructure; -import org.apache.uima.cas.Type; -import org.apache.uima.cas.text.AnnotationFS; -import org.apache.uima.fit.util.CasUtil; import org.apache.uima.fit.util.FSUtil; import org.apache.uima.jcas.cas.TOP; -import org.apache.uima.jcas.tcas.Annotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode; -import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; -import de.tudarmstadt.ukp.dkpro.core.api.segmentation.TrimUtils; -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.relation.RelationLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.AutoAcceptMode; import de.tudarmstadt.ukp.inception.recommendation.api.model.MetadataSuggestion; -import de.tudarmstadt.ukp.inception.recommendation.api.model.Offset; 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.schema.api.adapter.AnnotationComparisonUtils; +import de.tudarmstadt.ukp.inception.recommendation.relation.RelationSuggestionSupport; +import de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport; import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; public class SuggestionExtraction @@ -70,68 +45,7 @@ public class SuggestionExtraction private static final String AUTO_ACCEPT_ON_FIRST_ACCESS = "on-first-access"; - static final class ExtractionContext - { - private final int generation; - - private final SourceDocument document; - private final CAS originalCas; - private final CAS predictionCas; - private final String documentText; - - private final Recommender recommender; - - private final AnnotationLayer layer; - private final String typeName; - private final String featureName; - - private final Type predictedType; - - private final Feature labelFeature; - private final Feature sourceFeature; - private final Feature targetFeature; - private final Feature scoreFeature; - private final Feature scoreExplanationFeature; - private final Feature modeFeature; - private final Feature predictionFeature; - - private final boolean isMultiLabels; - - private final List result; - - ExtractionContext(int aGeneration, Recommender aRecommender, SourceDocument aDocument, - CAS aOriginalCas, CAS aPredictionCas) - { - recommender = aRecommender; - - document = aDocument; - originalCas = aOriginalCas; - documentText = originalCas.getDocumentText(); - predictionCas = aPredictionCas; - - generation = aGeneration; - layer = aRecommender.getLayer(); - featureName = aRecommender.getFeature().getName(); - typeName = layer.getName(); - - predictedType = CasUtil.getType(aPredictionCas, typeName); - labelFeature = predictedType.getFeatureByBaseName(featureName); - sourceFeature = predictedType.getFeatureByBaseName(FEAT_REL_SOURCE); - targetFeature = predictedType.getFeatureByBaseName(FEAT_REL_TARGET); - scoreFeature = predictedType - .getFeatureByBaseName(featureName + FEATURE_NAME_SCORE_SUFFIX); - scoreExplanationFeature = predictedType - .getFeatureByBaseName(featureName + FEATURE_NAME_SCORE_EXPLANATION_SUFFIX); - modeFeature = predictedType - .getFeatureByBaseName(featureName + FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX); - predictionFeature = predictedType.getFeatureByBaseName(FEATURE_NAME_IS_PREDICTION); - isMultiLabels = TYPE_NAME_STRING_ARRAY.equals(labelFeature.getRange().getName()); - - result = new ArrayList(); - } - } - - private static AutoAcceptMode getAutoAcceptMode(FeatureStructure aFS, Feature aModeFeature) + public static AutoAcceptMode getAutoAcceptMode(FeatureStructure aFS, Feature aModeFeature) { var autoAcceptMode = AutoAcceptMode.NEVER; var autoAcceptFeatureValue = aFS.getStringValue(aModeFeature); @@ -144,7 +58,7 @@ private static AutoAcceptMode getAutoAcceptMode(FeatureStructure aFS, Feature aM return autoAcceptMode; } - private static String[] getPredictedLabels(FeatureStructure predictedFS, + public static String[] getPredictedLabels(FeatureStructure predictedFS, Feature predictedFeature, boolean isStringMultiValue) { if (isStringMultiValue) { @@ -156,98 +70,24 @@ private static String[] getPredictedLabels(FeatureStructure predictedFS, static void extractDocumentMetadataSuggestion(ExtractionContext ctx, TOP predictedFS) { - var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.modeFeature); - var labels = getPredictedLabels(predictedFS, ctx.labelFeature, ctx.isMultiLabels); - var score = predictedFS.getDoubleValue(ctx.scoreFeature); - var scoreExplanation = predictedFS.getStringValue(ctx.scoreExplanationFeature); + var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); + var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), ctx.isMultiLabels()); + var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); + var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); for (var label : labels) { var suggestion = MetadataSuggestion.builder() // .withId(MetadataSuggestion.NEW_ID) // - .withGeneration(ctx.generation) // - .withRecommender(ctx.recommender) // - .withDocumentName(ctx.document.getName()) // + .withGeneration(ctx.getGeneration()) // + .withRecommender(ctx.getRecommender()) // + .withDocumentName(ctx.getDocument().getName()) // .withLabel(label) // .withUiLabel(label) // .withScore(score) // .withScoreExplanation(scoreExplanation) // .withAutoAcceptMode(autoAcceptMode) // .build(); - ctx.result.add(suggestion); - } - } - - static void extractRelationSuggestion(ExtractionContext ctx, TOP predictedFS) - { - var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.modeFeature); - var labels = getPredictedLabels(predictedFS, ctx.labelFeature, ctx.isMultiLabels); - var score = predictedFS.getDoubleValue(ctx.scoreFeature); - var scoreExplanation = predictedFS.getStringValue(ctx.scoreExplanationFeature); - - var source = (AnnotationFS) predictedFS.getFeatureValue(ctx.sourceFeature); - var target = (AnnotationFS) predictedFS.getFeatureValue(ctx.targetFeature); - - var originalSource = findEquivalentSpan(ctx.originalCas, source); - var originalTarget = findEquivalentSpan(ctx.originalCas, target); - if (originalSource.isEmpty() || originalTarget.isEmpty()) { - LOG.debug("Unable to find source or target of predicted relation in original CAS"); - return; - } - - var position = new RelationPosition(originalSource.get(), originalTarget.get()); - - for (var label : labels) { - var suggestion = RelationSuggestion.builder() // - .withId(RelationSuggestion.NEW_ID) // - .withGeneration(ctx.generation) // - .withRecommender(ctx.recommender) // - .withDocumentName(ctx.document.getName()) // - .withPosition(position) // - .withLabel(label) // - .withUiLabel(label) // - .withScore(score) // - .withScoreExplanation(scoreExplanation) // - .withAutoAcceptMode(autoAcceptMode) // - .build(); - ctx.result.add(suggestion); - } - } - - static void extractSpanSuggestion(ExtractionContext ctx, TOP predictedFS) - { - var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.modeFeature); - var labels = getPredictedLabels(predictedFS, ctx.labelFeature, ctx.isMultiLabels); - var score = predictedFS.getDoubleValue(ctx.scoreFeature); - var scoreExplanation = predictedFS.getStringValue(ctx.scoreExplanationFeature); - - var predictedAnnotation = (Annotation) predictedFS; - var targetOffsets = getOffsets(ctx.layer.getAnchoringMode(), ctx.originalCas, - predictedAnnotation); - - if (targetOffsets.isEmpty()) { - LOG.debug("Prediction cannot be anchored to [{}]: {}", ctx.layer.getAnchoringMode(), - predictedAnnotation); - return; - } - - var offsets = targetOffsets.get(); - var coveredText = ctx.documentText.substring(offsets.getBegin(), offsets.getEnd()); - - for (var label : labels) { - var suggestion = SpanSuggestion.builder() // - .withId(RelationSuggestion.NEW_ID) // - .withGeneration(ctx.generation) // - .withRecommender(ctx.recommender) // - .withDocumentName(ctx.document.getName()) // - .withPosition(offsets) // - .withCoveredText(coveredText) // - .withLabel(label) // - .withUiLabel(label) // - .withScore(score) // - .withScoreExplanation(scoreExplanation) // - .withAutoAcceptMode(autoAcceptMode) // - .build(); - ctx.result.add(suggestion); + ctx.getResult().add(suggestion); } } @@ -257,18 +97,18 @@ public static List extractSuggestions(int aGeneration, CAS var ctx = new ExtractionContext(aGeneration, aRecommender, aDocument, aOriginalCas, aPredictionCas); - for (var predictedFS : aPredictionCas.select(ctx.predictedType)) { - if (!predictedFS.getBooleanValue(ctx.predictionFeature)) { + for (var predictedFS : aPredictionCas.select(ctx.getPredictedType())) { + if (!predictedFS.getBooleanValue(ctx.getPredictionFeature())) { continue; } - switch (ctx.layer.getType()) { + switch (ctx.getLayer().getType()) { case SpanLayerSupport.TYPE: { - extractSpanSuggestion(ctx, predictedFS); + SpanSuggestionSupport.extractSuggestion(ctx, predictedFS); break; } case RelationLayerSupport.TYPE: { - extractRelationSuggestion(ctx, predictedFS); + RelationSuggestionSupport.extractSuggestion(ctx, predictedFS); break; } case DocumentMetadataLayerSupport.TYPE: { @@ -277,168 +117,10 @@ public static List extractSuggestions(int aGeneration, CAS } default: throw new IllegalStateException( - "Unsupported layer type [" + ctx.layer.getType() + "]"); - } - } - - return ctx.result; - } - - /** - * Calculates the offsets of the given predicted annotation in the original CAS . - * - * @param aMode - * the anchoring mode of the target layer - * @param aOriginalCas - * the original CAS. - * @param aPredictedAnnotation - * the predicted annotation. - * @return the proper offsets. - */ - static Optional getOffsets(AnchoringMode aMode, CAS aOriginalCas, - Annotation aPredictedAnnotation) - { - switch (aMode) { - case CHARACTERS: { - return getOffsetsAnchoredOnCharacters(aOriginalCas, aPredictedAnnotation); - } - case SINGLE_TOKEN: { - return getOffsetsAnchoredOnSingleTokens(aOriginalCas, aPredictedAnnotation); - } - case TOKENS: { - return getOffsetsAnchoredOnTokens(aOriginalCas, aPredictedAnnotation); - } - case SENTENCES: { - return getOffsetsAnchoredOnSentences(aOriginalCas, aPredictedAnnotation); - } - default: - throw new IllegalStateException("Unsupported anchoring mode: [" + aMode + "]"); - } - } - - private static Optional getOffsetsAnchoredOnCharacters(CAS aOriginalCas, - Annotation aPredictedAnnotation) - { - int[] offsets = { max(aPredictedAnnotation.getBegin(), 0), - min(aOriginalCas.getDocumentText().length(), aPredictedAnnotation.getEnd()) }; - TrimUtils.trim(aPredictedAnnotation.getCAS().getDocumentText(), offsets); - var begin = offsets[0]; - var end = offsets[1]; - return Optional.of(new Offset(begin, end)); - } - - private static Optional getOffsetsAnchoredOnSentences(CAS aOriginalCas, - Annotation aPredictedAnnotation) - { - var sentences = aOriginalCas.select(Sentence.class) // - .coveredBy(aPredictedAnnotation) // - .asList(); - - if (sentences.isEmpty()) { - // This can happen if a recommender uses different token boundaries (e.g. if a - // remote service performs its own tokenization). We might be smart here by - // looking for overlapping sentences instead of covered sentences. - LOG.trace("Discarding suggestion because no covered sentences were found: {}", - aPredictedAnnotation); - return Optional.empty(); - } - - var begin = sentences.get(0).getBegin(); - var end = sentences.get(sentences.size() - 1).getEnd(); - return Optional.of(new Offset(begin, end)); - } - - private static Optional getOffsetsAnchoredOnSingleTokens(CAS aOriginalCas, - Annotation aPredictedAnnotation) - { - Type tokenType = getType(aOriginalCas, Token.class); - var tokens = aOriginalCas. select(tokenType) // - .coveredBy(aPredictedAnnotation) // - .limit(2).asList(); - - if (tokens.isEmpty()) { - // This can happen if a recommender uses different token boundaries (e.g. if a - // remote service performs its own tokenization). We might be smart here by - // looking for overlapping tokens instead of contained tokens. - LOG.trace("Discarding suggestion because no covering token was found: {}", - aPredictedAnnotation); - return Optional.empty(); - } - - if (tokens.size() > 1) { - // We only want to accept single-token suggestions - LOG.trace("Discarding suggestion because only single-token suggestions are " - + "accepted: {}", aPredictedAnnotation); - return Optional.empty(); - } - - AnnotationFS token = tokens.get(0); - var begin = token.getBegin(); - var end = token.getEnd(); - return Optional.of(new Offset(begin, end)); - } - - public static Optional getOffsetsAnchoredOnTokens(CAS aOriginalCas, - Annotation aPredictedAnnotation) - { - var tokens = aOriginalCas.select(Token.class) // - .coveredBy(aPredictedAnnotation) // - .asList(); - - if (tokens.isEmpty()) { - if (aPredictedAnnotation.getBegin() == aPredictedAnnotation.getEnd()) { - var pos = aPredictedAnnotation.getBegin(); - var allTokens = aOriginalCas.select(Token.class).asList(); - Token prevToken = null; - for (var token : allTokens) { - if (prevToken == null && pos < token.getBegin()) { - return Optional.of(new Offset(token.getBegin(), token.getBegin())); - } - - if (token.covering(aPredictedAnnotation)) { - return Optional.of(new Offset(pos, pos)); - } - - if (prevToken != null && pos < token.getBegin()) { - return Optional.of(new Offset(prevToken.getEnd(), prevToken.getEnd())); - } - - prevToken = token; - } - - if (prevToken != null && pos >= prevToken.getEnd()) { - return Optional.of(new Offset(prevToken.getEnd(), prevToken.getEnd())); - } + "Unsupported layer type [" + ctx.getLayer().getType() + "]"); } - - // This can happen if a recommender uses different token boundaries (e.g. if a - // remote service performs its own tokenization). We might be smart here by - // looking for overlapping tokens instead of covered tokens. - LOG.trace("Discarding suggestion because no covered tokens were found: {}", - aPredictedAnnotation); - return Optional.empty(); } - var begin = tokens.get(0).getBegin(); - var end = tokens.get(tokens.size() - 1).getEnd(); - return Optional.of(new Offset(begin, end)); - } - - /** - * 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(); + return ctx.getResult(); } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java index 7d3679a3c5d..4983e4acf3b 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java @@ -22,9 +22,12 @@ import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_TRANSIENT_REJECTED; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction.REJECTED; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction.SKIPPED; +import static java.lang.Math.max; +import static java.lang.Math.min; import static java.util.Comparator.comparingInt; import static java.util.stream.Collectors.toList; import static org.apache.uima.cas.text.AnnotationPredicates.colocated; +import static org.apache.uima.fit.util.CasUtil.getType; import static org.apache.uima.fit.util.CasUtil.select; import java.lang.invoke.MethodHandles; @@ -45,15 +48,20 @@ import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.cas.text.AnnotationPredicates; import org.apache.uima.fit.util.CasUtil; +import org.apache.uima.jcas.cas.TOP; import org.apache.uima.jcas.tcas.Annotation; 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.AnchoringMode; 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.dkpro.core.api.segmentation.TrimUtils; +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.span.SpanAdapter; import de.tudarmstadt.ukp.inception.recommendation.api.LearningRecordService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; @@ -65,9 +73,12 @@ 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.Offset; +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.SuggestionGroup; import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderProperties; +import de.tudarmstadt.ukp.inception.recommendation.service.ExtractionContext; +import de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction; import de.tudarmstadt.ukp.inception.recommendation.util.OverlapIterator; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; @@ -449,4 +460,184 @@ public Optional getRenderer() return Optional.of(new SpanSuggestionRenderer(recommendationService, schemaService, featureSupportRegistry, recommenderProperties)); } + + public static void extractSuggestion(ExtractionContext ctx, TOP predictedFS) + { + var autoAcceptMode = SuggestionExtraction.getAutoAcceptMode(predictedFS, + ctx.getModeFeature()); + var labels = SuggestionExtraction.getPredictedLabels(predictedFS, ctx.getLabelFeature(), + ctx.isMultiLabels()); + var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); + var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); + + var predictedAnnotation = (Annotation) predictedFS; + var targetOffsets = getOffsets(ctx.getLayer().getAnchoringMode(), ctx.getOriginalCas(), + predictedAnnotation); + + if (targetOffsets.isEmpty()) { + LOG.debug("Prediction cannot be anchored to [{}]: {}", + ctx.getLayer().getAnchoringMode(), predictedAnnotation); + return; + } + + var offsets = targetOffsets.get(); + var coveredText = ctx.getDocumentText().substring(offsets.getBegin(), offsets.getEnd()); + + for (var label : labels) { + var suggestion = SpanSuggestion.builder() // + .withId(RelationSuggestion.NEW_ID) // + .withGeneration(ctx.getGeneration()) // + .withRecommender(ctx.getRecommender()) // + .withDocumentName(ctx.getDocument().getName()) // + .withPosition(offsets) // + .withCoveredText(coveredText) // + .withLabel(label) // + .withUiLabel(label) // + .withScore(score) // + .withScoreExplanation(scoreExplanation) // + .withAutoAcceptMode(autoAcceptMode) // + .build(); + ctx.getResult().add(suggestion); + } + } + + /** + * Calculates the offsets of the given predicted annotation in the original CAS . + * + * @param aMode + * the anchoring mode of the target layer + * @param aOriginalCas + * the original CAS. + * @param aPredictedAnnotation + * the predicted annotation. + * @return the proper offsets. + */ + static Optional getOffsets(AnchoringMode aMode, CAS aOriginalCas, + Annotation aPredictedAnnotation) + { + switch (aMode) { + case CHARACTERS: { + return getOffsetsAnchoredOnCharacters(aOriginalCas, aPredictedAnnotation); + } + case SINGLE_TOKEN: { + return getOffsetsAnchoredOnSingleTokens(aOriginalCas, aPredictedAnnotation); + } + case TOKENS: { + return getOffsetsAnchoredOnTokens(aOriginalCas, aPredictedAnnotation); + } + case SENTENCES: { + return getOffsetsAnchoredOnSentences(aOriginalCas, aPredictedAnnotation); + } + default: + throw new IllegalStateException("Unsupported anchoring mode: [" + aMode + "]"); + } + } + + private static Optional getOffsetsAnchoredOnCharacters(CAS aOriginalCas, + Annotation aPredictedAnnotation) + { + int[] offsets = { max(aPredictedAnnotation.getBegin(), 0), + min(aOriginalCas.getDocumentText().length(), aPredictedAnnotation.getEnd()) }; + TrimUtils.trim(aPredictedAnnotation.getCAS().getDocumentText(), offsets); + var begin = offsets[0]; + var end = offsets[1]; + return Optional.of(new Offset(begin, end)); + } + + private static Optional getOffsetsAnchoredOnSentences(CAS aOriginalCas, + Annotation aPredictedAnnotation) + { + var sentences = aOriginalCas.select(Sentence.class) // + .coveredBy(aPredictedAnnotation) // + .asList(); + + if (sentences.isEmpty()) { + // This can happen if a recommender uses different token boundaries (e.g. if a + // remote service performs its own tokenization). We might be smart here by + // looking for overlapping sentences instead of covered sentences. + LOG.trace("Discarding suggestion because no covered sentences were found: {}", + aPredictedAnnotation); + return Optional.empty(); + } + + var begin = sentences.get(0).getBegin(); + var end = sentences.get(sentences.size() - 1).getEnd(); + return Optional.of(new Offset(begin, end)); + } + + private static Optional getOffsetsAnchoredOnSingleTokens(CAS aOriginalCas, + Annotation aPredictedAnnotation) + { + Type tokenType = getType(aOriginalCas, Token.class); + var tokens = aOriginalCas. select(tokenType) // + .coveredBy(aPredictedAnnotation) // + .limit(2).asList(); + + if (tokens.isEmpty()) { + // This can happen if a recommender uses different token boundaries (e.g. if a + // remote service performs its own tokenization). We might be smart here by + // looking for overlapping tokens instead of contained tokens. + LOG.trace("Discarding suggestion because no covering token was found: {}", + aPredictedAnnotation); + return Optional.empty(); + } + + if (tokens.size() > 1) { + // We only want to accept single-token suggestions + LOG.trace("Discarding suggestion because only single-token suggestions are " + + "accepted: {}", aPredictedAnnotation); + return Optional.empty(); + } + + AnnotationFS token = tokens.get(0); + var begin = token.getBegin(); + var end = token.getEnd(); + return Optional.of(new Offset(begin, end)); + } + + public static Optional getOffsetsAnchoredOnTokens(CAS aOriginalCas, + Annotation aPredictedAnnotation) + { + var tokens = aOriginalCas.select(Token.class) // + .coveredBy(aPredictedAnnotation) // + .asList(); + + if (tokens.isEmpty()) { + if (aPredictedAnnotation.getBegin() == aPredictedAnnotation.getEnd()) { + var pos = aPredictedAnnotation.getBegin(); + var allTokens = aOriginalCas.select(Token.class).asList(); + Token prevToken = null; + for (var token : allTokens) { + if (prevToken == null && pos < token.getBegin()) { + return Optional.of(new Offset(token.getBegin(), token.getBegin())); + } + + if (token.covering(aPredictedAnnotation)) { + return Optional.of(new Offset(pos, pos)); + } + + if (prevToken != null && pos < token.getBegin()) { + return Optional.of(new Offset(prevToken.getEnd(), prevToken.getEnd())); + } + + prevToken = token; + } + + if (prevToken != null && pos >= prevToken.getEnd()) { + return Optional.of(new Offset(prevToken.getEnd(), prevToken.getEnd())); + } + } + + // This can happen if a recommender uses different token boundaries (e.g. if a + // remote service performs its own tokenization). We might be smart here by + // looking for overlapping tokens instead of covered tokens. + LOG.trace("Discarding suggestion because no covered tokens were found: {}", + aPredictedAnnotation); + return Optional.empty(); + } + + var begin = tokens.get(0).getBegin(); + var end = tokens.get(tokens.size() - 1).getEnd(); + return Optional.of(new Offset(begin, end)); + } } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java index 12f0fea6867..9609e383c55 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java @@ -27,7 +27,6 @@ import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation.DETAIL_EDITOR; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation.MAIN_EDITOR; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction.ACCEPTED; -import static de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction.getOffsetsAnchoredOnTokens; import static java.util.Arrays.asList; import static org.apache.uima.fit.factory.JCasFactory.createJCas; import static org.apache.uima.fit.factory.JCasFactory.createText; @@ -43,10 +42,8 @@ import org.apache.uima.cas.CAS; import org.apache.uima.cas.Feature; import org.apache.uima.cas.Type; -import org.apache.uima.fit.testing.factory.TokenBuilder; import org.apache.uima.fit.util.CasUtil; import org.apache.uima.jcas.JCas; -import org.apache.uima.jcas.tcas.Annotation; import org.apache.uima.resource.metadata.TypeSystemDescription; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -67,8 +64,6 @@ import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; -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.string.StringFeatureSupport; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; @@ -82,8 +77,6 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory; import de.tudarmstadt.ukp.inception.recommendation.relation.RelationSuggestionSupport; -import de.tudarmstadt.ukp.inception.recommendation.service.LayerRecommendtionSupportRegistryImpl; -import de.tudarmstadt.ukp.inception.recommendation.service.RecommendationServiceImpl; import de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport; import de.tudarmstadt.ukp.inception.schema.api.layer.LayerSupportRegistry; import de.tudarmstadt.ukp.inception.schema.service.AnnotationSchemaServiceImpl; @@ -249,34 +242,6 @@ public void monkeyPatchTypeSystem_WithNer_CreatesScoreFeatures() throws Exceptio } } - @Test - void thatZeroWithAnnotationsAreCorrectlyAnchoredOnTokens() throws Exception - { - var jCas = createJCas(); - TokenBuilder.create(Token.class, Sentence.class).buildTokens(jCas, " This is a test. "); - var textLength = jCas.getDocumentText().length(); - var tokens = jCas.select(Token.class).asList(); - var firstTokenBegin = tokens.get(0).getBegin(); - var lastTokenEnd = tokens.get(tokens.size() - 1).getEnd(); - - assertThat(getOffsetsAnchoredOnTokens(jCas.getCas(), new Annotation(jCas, 0, 0))).get() - .as("Zero-width annotation before first token snaps to first token start") - .isEqualTo(new Offset(firstTokenBegin, firstTokenBegin)); - - assertThat(getOffsetsAnchoredOnTokens(jCas.getCas(), - new Annotation(jCas, textLength, textLength))).get() - .as("Zero-width annotation after last token snaps to last token end") - .isEqualTo(new Offset(lastTokenEnd, lastTokenEnd)); - - assertThat(getOffsetsAnchoredOnTokens(jCas.getCas(), new Annotation(jCas, 4, 4))).get() - .as("Zero-width annotation within token remains") // - .isEqualTo(new Offset(4, 4)); - - assertThat(getOffsetsAnchoredOnTokens(jCas.getCas(), new Annotation(jCas, 10, 10))).get() - .as("Zero-width annotation between tokens snaps to end of previous") // - .isEqualTo(new Offset(9, 9)); - } - @Test void testUpsertSpanFeature() throws Exception { diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java index f5b1e964ffc..f4c65e5a99a 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java @@ -17,16 +17,12 @@ */ package de.tudarmstadt.ukp.inception.recommendation.service; -import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.CHARACTERS; -import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.SENTENCES; -import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.SINGLE_TOKEN; import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.TOKENS; import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX; import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; 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 de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction.extractSuggestions; -import static de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction.getOffsets; import static de.tudarmstadt.ukp.inception.support.uima.FeatureStructureBuilder.buildFS; import static java.util.Arrays.asList; import static org.apache.uima.cas.CAS.TYPE_NAME_ANNOTATION; @@ -42,8 +38,6 @@ import java.util.Arrays; import org.apache.uima.UIMAFramework; -import org.apache.uima.fit.factory.CasFactory; -import org.apache.uima.fit.factory.JCasFactory; import org.apache.uima.fit.testing.factory.TokenBuilder; import org.apache.uima.jcas.tcas.Annotation; import org.junit.jupiter.api.BeforeEach; @@ -70,9 +64,6 @@ class RecommendationServiceImplTest private SourceDocument doc1; private SourceDocument doc2; private AnnotationLayer layer1; - private AnnotationLayer layer2; - private AnnotationFeature feature1; - private AnnotationFeature feature2; @BeforeEach void setup() @@ -83,9 +74,6 @@ void setup() doc2 = new SourceDocument("doc2", null, null); doc2.setId(2l); layer1 = AnnotationLayer.builder().withId(1l).withName("layer1").build(); - layer2 = AnnotationLayer.builder().withId(2l).withName("layer2").build(); - feature1 = AnnotationFeature.builder().withName("feat1").build(); - feature2 = AnnotationFeature.builder().withName("feat2").build(); } @Test @@ -142,106 +130,6 @@ void testReconciliation() throws Exception .containsExactlyInAnyOrder(tuple(0, "aged", 1), tuple(3, "added", 0)); } - @Test - void testOffsetAlignmentWithAnchoringOnCharacters() throws Exception - { - var mode = CHARACTERS; - var targetCas = CasFactory.createCas(); - tokenBuilder.buildTokens(targetCas.getJCas(), "This is a test ."); - - var suggestionCas = JCasFactory.createText(targetCas.getDocumentText()); - - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 1))) // - .as("Trivial case: one character") // - .get().isEqualTo(new Offset(0, 1)); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 4, 8))) // - .as("Leading and trailing space should be removed") // - .get().isEqualTo(new Offset(5, 7)); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, -10, 100))) // - .as("Range should be clipped to document boundaries").get() - .isEqualTo(new Offset(0, targetCas.getDocumentText().length())); - } - - @Test - void testOffsetAlignmentWithAnchoringOnSingleToken() throws Exception - { - var mode = SINGLE_TOKEN; - var targetCas = CasFactory.createCas(); - tokenBuilder.buildTokens(targetCas.getJCas(), "This is a test ."); - - var suggestionCas = JCasFactory.createText(targetCas.getDocumentText()); - - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 1))) // - .as("Reduce to empty if no token is fully covered") // - .isEmpty(); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 4))) // - .as("Trivial case: one token") // - .get().isEqualTo(new Offset(0, 4)); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 7))) // - .as("Discard suggestion if it covers more than one token") // - .isEmpty(); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, -10, 100))) // - .as("Discard suggestion if it covers more than one token (2)") // - .isEmpty(); - } - - @Test - void testOffsetAlignmentWithAnchoringOnTokens() throws Exception - { - var mode = TOKENS; - var targetCas = CasFactory.createCas(); - tokenBuilder.buildTokens(targetCas.getJCas(), "This is a test ."); - - var suggestionCas = JCasFactory.createText(targetCas.getDocumentText()); - - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 1))) // - .as("Reduce to empty if no token is fully covered") // - .isEmpty(); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 4))) // - .as("Trivial case: one token") // - .get().isEqualTo(new Offset(0, 4)); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 7))) // - .as("Trivial case: two tokens") // - .get().isEqualTo(new Offset(0, 7)); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 2, 12))) // - .as("Discard incompletely covered tokens") // - .get().isEqualTo(new Offset(5, 9)); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, -10, 100))) // - .as("Range should be clipped to document boundaries").get() - .isEqualTo(new Offset(0, targetCas.getDocumentText().length())); - } - - @Test - void testOffsetAlignmentWithAnchoringOnSentences() throws Exception - { - var mode = SENTENCES; - var targetCas = CasFactory.createCas(); - tokenBuilder.buildTokens(targetCas.getJCas(), "This is a test .\nAnother sentence here ."); - - var suggestionCas = JCasFactory.createText(targetCas.getDocumentText()); - var sentences = targetCas.select(Sentence.class).asList(); - var sent1 = sentences.get(0); - var sent2 = sentences.get(1); - - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 1))) // - .as("Reduce to empty if no sentence is fully covered") // - .isEmpty(); - assertThat(getOffsets(mode, targetCas, - new Annotation(suggestionCas, sent1.getBegin(), sent1.getEnd()))) // - .as("Trivial case: one sentence") // - .get().isEqualTo(new Offset(sent1.getBegin(), sent1.getEnd())); - assertThat(getOffsets(mode, targetCas, - new Annotation(suggestionCas, sent1.getBegin(), sent2.getEnd()))) // - .as("Trivial case: two sentences") // - .get().isEqualTo(new Offset(sent1.getBegin(), sent2.getEnd())); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 30))) // - .as("Discard incompletely covered sentences") // - .get().isEqualTo(new Offset(0, 16)); - assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, -10, 100))) // - .as("Range should be clipped to document boundaries").get() - .isEqualTo(new Offset(0, targetCas.getDocumentText().length())); - } - @Test void testExtractSuggestionsWithSpanSuggestions() throws Exception { diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java index 3716c9750a0..33ef1f5c1e5 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java @@ -17,13 +17,23 @@ */ package de.tudarmstadt.ukp.inception.recommendation.span; +import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.CHARACTERS; +import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.SENTENCES; +import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.SINGLE_TOKEN; +import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.TOKENS; import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; +import static de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport.getOffsets; +import static de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport.getOffsetsAnchoredOnTokens; import static de.tudarmstadt.ukp.inception.support.uima.AnnotationBuilder.buildAnnotation; +import static org.apache.uima.fit.factory.JCasFactory.createJCas; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; import org.apache.uima.cas.CAS; import org.apache.uima.fit.factory.CasFactory; +import org.apache.uima.fit.factory.JCasFactory; +import org.apache.uima.fit.testing.factory.TokenBuilder; +import org.apache.uima.jcas.tcas.Annotation; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,8 +42,11 @@ import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; +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.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderTypeSystemUtils; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Offset; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction; @@ -41,6 +54,7 @@ class SpanSuggestionExtractionTest { + private TokenBuilder tokenBuilder; private Project project; private SourceDocument document; private CAS originalCas; @@ -48,6 +62,7 @@ class SpanSuggestionExtractionTest @BeforeEach void setup() throws Exception { + tokenBuilder = new TokenBuilder<>(Token.class, Sentence.class); project = Project.builder() // .withId(1l) // .withName("Test") // @@ -106,4 +121,132 @@ void testSpanExtraction() throws Exception .containsExactly( // tuple(recommender.getName(), "verb")); } + + @Test + void thatZeroWithAnnotationsAreCorrectlyAnchoredOnTokens() throws Exception + { + var jCas = createJCas(); + TokenBuilder.create(Token.class, Sentence.class).buildTokens(jCas, " This is a test. "); + var textLength = jCas.getDocumentText().length(); + var tokens = jCas.select(Token.class).asList(); + var firstTokenBegin = tokens.get(0).getBegin(); + var lastTokenEnd = tokens.get(tokens.size() - 1).getEnd(); + + assertThat(getOffsetsAnchoredOnTokens(jCas.getCas(), new Annotation(jCas, 0, 0))).get() + .as("Zero-width annotation before first token snaps to first token start") + .isEqualTo(new Offset(firstTokenBegin, firstTokenBegin)); + + assertThat(getOffsetsAnchoredOnTokens(jCas.getCas(), + new Annotation(jCas, textLength, textLength))).get() + .as("Zero-width annotation after last token snaps to last token end") + .isEqualTo(new Offset(lastTokenEnd, lastTokenEnd)); + + assertThat(getOffsetsAnchoredOnTokens(jCas.getCas(), new Annotation(jCas, 4, 4))).get() + .as("Zero-width annotation within token remains") // + .isEqualTo(new Offset(4, 4)); + + assertThat(getOffsetsAnchoredOnTokens(jCas.getCas(), new Annotation(jCas, 10, 10))).get() + .as("Zero-width annotation between tokens snaps to end of previous") // + .isEqualTo(new Offset(9, 9)); + } + + @Test + void testOffsetAlignmentWithAnchoringOnCharacters() throws Exception + { + var mode = CHARACTERS; + var targetCas = CasFactory.createCas(); + tokenBuilder.buildTokens(targetCas.getJCas(), "This is a test ."); + + var suggestionCas = JCasFactory.createText(targetCas.getDocumentText()); + + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 1))) // + .as("Trivial case: one character") // + .get().isEqualTo(new Offset(0, 1)); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 4, 8))) // + .as("Leading and trailing space should be removed") // + .get().isEqualTo(new Offset(5, 7)); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, -10, 100))) // + .as("Range should be clipped to document boundaries").get() + .isEqualTo(new Offset(0, targetCas.getDocumentText().length())); + } + + @Test + void testOffsetAlignmentWithAnchoringOnSingleToken() throws Exception + { + var mode = SINGLE_TOKEN; + var targetCas = CasFactory.createCas(); + tokenBuilder.buildTokens(targetCas.getJCas(), "This is a test ."); + + var suggestionCas = JCasFactory.createText(targetCas.getDocumentText()); + + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 1))) // + .as("Reduce to empty if no token is fully covered") // + .isEmpty(); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 4))) // + .as("Trivial case: one token") // + .get().isEqualTo(new Offset(0, 4)); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 7))) // + .as("Discard suggestion if it covers more than one token") // + .isEmpty(); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, -10, 100))) // + .as("Discard suggestion if it covers more than one token (2)") // + .isEmpty(); + } + + @Test + void testOffsetAlignmentWithAnchoringOnTokens() throws Exception + { + var mode = TOKENS; + var targetCas = CasFactory.createCas(); + tokenBuilder.buildTokens(targetCas.getJCas(), "This is a test ."); + + var suggestionCas = JCasFactory.createText(targetCas.getDocumentText()); + + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 1))) // + .as("Reduce to empty if no token is fully covered") // + .isEmpty(); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 4))) // + .as("Trivial case: one token") // + .get().isEqualTo(new Offset(0, 4)); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 7))) // + .as("Trivial case: two tokens") // + .get().isEqualTo(new Offset(0, 7)); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 2, 12))) // + .as("Discard incompletely covered tokens") // + .get().isEqualTo(new Offset(5, 9)); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, -10, 100))) // + .as("Range should be clipped to document boundaries").get() + .isEqualTo(new Offset(0, targetCas.getDocumentText().length())); + } + + @Test + void testOffsetAlignmentWithAnchoringOnSentences() throws Exception + { + var mode = SENTENCES; + var targetCas = CasFactory.createCas(); + tokenBuilder.buildTokens(targetCas.getJCas(), "This is a test .\nAnother sentence here ."); + + var suggestionCas = JCasFactory.createText(targetCas.getDocumentText()); + var sentences = targetCas.select(Sentence.class).asList(); + var sent1 = sentences.get(0); + var sent2 = sentences.get(1); + + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 1))) // + .as("Reduce to empty if no sentence is fully covered") // + .isEmpty(); + assertThat(getOffsets(mode, targetCas, + new Annotation(suggestionCas, sent1.getBegin(), sent1.getEnd()))) // + .as("Trivial case: one sentence") // + .get().isEqualTo(new Offset(sent1.getBegin(), sent1.getEnd())); + assertThat(getOffsets(mode, targetCas, + new Annotation(suggestionCas, sent1.getBegin(), sent2.getEnd()))) // + .as("Trivial case: two sentences") // + .get().isEqualTo(new Offset(sent1.getBegin(), sent2.getEnd())); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, 0, 30))) // + .as("Discard incompletely covered sentences") // + .get().isEqualTo(new Offset(0, 16)); + assertThat(getOffsets(mode, targetCas, new Annotation(suggestionCas, -10, 100))) // + .as("Range should be clipped to document boundaries").get() + .isEqualTo(new Offset(0, targetCas.getDocumentText().length())); + } } From a34fcffedafdaddef278eb348fa9e6d4fd22e7d6 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Sun, 24 Dec 2023 20:29:53 +0100 Subject: [PATCH 08/13] #2696 - Document-level recommendations - Continue moving extraction logic into the suggestion supports --- .../sidebar/MetadataSuggestionSupport.java | 25 ++++++++ .../api/SuggestionSupport_ImplBase.java | 29 +++++++++ .../api/model/SuggestionDocumentGroup.java | 4 +- .../api/recommender}/ExtractionContext.java | 4 +- .../relation/RelationSuggestionSupport.java | 9 +-- .../service/SuggestionExtraction.java | 63 +------------------ .../span/SpanSuggestionSupport.java | 9 +-- ...onSuggestionVisibilityCalculationTest.java | 1 - ...anSuggestionVisibilityCalculationTest.java | 1 - 9 files changed, 67 insertions(+), 78 deletions(-) rename inception/{inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service => inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender}/ExtractionContext.java (98%) diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java index a7a313fa2ca..c15e30476f9 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java @@ -33,6 +33,7 @@ import org.apache.uima.cas.CAS; import org.apache.uima.cas.Feature; import org.apache.uima.jcas.cas.AnnotationBase; +import org.apache.uima.jcas.cas.TOP; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; @@ -51,6 +52,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction; import de.tudarmstadt.ukp.inception.recommendation.api.model.MetadataSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.ExtractionContext; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; @@ -277,4 +279,27 @@ public Optional getRenderer() { return Optional.empty(); } + + public static void extractSuggestion(ExtractionContext ctx, TOP predictedFS) + { + var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); + var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), ctx.isMultiLabels()); + var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); + var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); + + for (var label : labels) { + var suggestion = MetadataSuggestion.builder() // + .withId(MetadataSuggestion.NEW_ID) // + .withGeneration(ctx.getGeneration()) // + .withRecommender(ctx.getRecommender()) // + .withDocumentName(ctx.getDocument().getName()) // + .withLabel(label) // + .withUiLabel(label) // + .withScore(score) // + .withScoreExplanation(scoreExplanation) // + .withAutoAcceptMode(autoAcceptMode) // + .build(); + ctx.getResult().add(suggestion); + } + } } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport_ImplBase.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport_ImplBase.java index 03159fcc7d8..a352059588c 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport_ImplBase.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport_ImplBase.java @@ -23,6 +23,9 @@ import org.apache.uima.cas.AnnotationBaseFS; import org.apache.uima.cas.CAS; +import org.apache.uima.cas.Feature; +import org.apache.uima.cas.FeatureStructure; +import org.apache.uima.fit.util.FSUtil; import org.springframework.beans.factory.BeanNameAware; import org.springframework.context.ApplicationEventPublisher; @@ -30,6 +33,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.recommendation.api.event.RecommendationAcceptedEvent; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.AutoAcceptMode; import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation; import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @@ -95,4 +99,29 @@ protected void commmitLabel(String aSessionOwner, SourceDocument aDocument, Stri annotation, aFeature, aSuggestion.getLabel())); } } + + private static final String AUTO_ACCEPT_ON_FIRST_ACCESS = "on-first-access"; + + public static AutoAcceptMode getAutoAcceptMode(FeatureStructure aFS, Feature aModeFeature) + { + var autoAcceptMode = AutoAcceptMode.NEVER; + var autoAcceptFeatureValue = aFS.getStringValue(aModeFeature); + if (autoAcceptFeatureValue != null) { + switch (autoAcceptFeatureValue) { + case AUTO_ACCEPT_ON_FIRST_ACCESS: + autoAcceptMode = AutoAcceptMode.ON_FIRST_ACCESS; + } + } + return autoAcceptMode; + } + + public static String[] getPredictedLabels(FeatureStructure predictedFS, + Feature predictedFeature, boolean isStringMultiValue) + { + if (isStringMultiValue) { + return FSUtil.getFeature(predictedFS, predictedFeature, String[].class); + } + + return new String[] { predictedFS.getFeatureValueAsString(predictedFeature) }; + } } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java index c79d7fc5ba0..91ae9110dde 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java @@ -77,8 +77,8 @@ public static SuggestionDocumentGroup groups * the list to retrieve suggestions from * @return a SuggestionDocumentGroup where only suggestions of type V are added */ - public static Map, SuggestionDocumentGroup> groupByType( - List aSuggestions) + public static Map, SuggestionDocumentGroup> // + groupByType(List aSuggestions) { if (aSuggestions == null || aSuggestions.isEmpty()) { return Collections.emptyMap(); diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/ExtractionContext.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/ExtractionContext.java similarity index 98% rename from inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/ExtractionContext.java rename to inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/ExtractionContext.java index 8f8774585bf..58777e21480 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/ExtractionContext.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/ExtractionContext.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.service; +package de.tudarmstadt.ukp.inception.recommendation.api.recommender; import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX; import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; @@ -191,4 +191,4 @@ public List getResult() { return result; } -} \ No newline at end of file +} 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 475555ffe33..98db34b99ee 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 @@ -62,8 +62,7 @@ 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.service.ExtractionContext; -import de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.ExtractionContext; 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; @@ -380,10 +379,8 @@ public Optional getRenderer() public static void extractSuggestion(ExtractionContext ctx, TOP predictedFS) { - var autoAcceptMode = SuggestionExtraction.getAutoAcceptMode(predictedFS, - ctx.getModeFeature()); - var labels = SuggestionExtraction.getPredictedLabels(predictedFS, ctx.getLabelFeature(), - ctx.isMultiLabels()); + var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); + var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), ctx.isMultiLabels()); var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java index 2e189832c3f..e4b0914ef31 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java @@ -17,80 +17,23 @@ */ package de.tudarmstadt.ukp.inception.recommendation.service; -import java.lang.invoke.MethodHandles; import java.util.List; import org.apache.uima.cas.CAS; -import org.apache.uima.cas.Feature; -import org.apache.uima.cas.FeatureStructure; -import org.apache.uima.fit.util.FSUtil; -import org.apache.uima.jcas.cas.TOP; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; -import de.tudarmstadt.ukp.inception.recommendation.api.model.AutoAcceptMode; -import de.tudarmstadt.ukp.inception.recommendation.api.model.MetadataSuggestion; 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.RelationSuggestionSupport; import de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport; import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; +import de.tudarmstadt.ukp.inception.ui.core.docanno.sidebar.MetadataSuggestionSupport; public class SuggestionExtraction { - private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - private static final String AUTO_ACCEPT_ON_FIRST_ACCESS = "on-first-access"; - - public static AutoAcceptMode getAutoAcceptMode(FeatureStructure aFS, Feature aModeFeature) - { - var autoAcceptMode = AutoAcceptMode.NEVER; - var autoAcceptFeatureValue = aFS.getStringValue(aModeFeature); - if (autoAcceptFeatureValue != null) { - switch (autoAcceptFeatureValue) { - case AUTO_ACCEPT_ON_FIRST_ACCESS: - autoAcceptMode = AutoAcceptMode.ON_FIRST_ACCESS; - } - } - return autoAcceptMode; - } - - public static String[] getPredictedLabels(FeatureStructure predictedFS, - Feature predictedFeature, boolean isStringMultiValue) - { - if (isStringMultiValue) { - return FSUtil.getFeature(predictedFS, predictedFeature, String[].class); - } - - return new String[] { predictedFS.getFeatureValueAsString(predictedFeature) }; - } - - static void extractDocumentMetadataSuggestion(ExtractionContext ctx, TOP predictedFS) - { - var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); - var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), ctx.isMultiLabels()); - var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); - var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); - - for (var label : labels) { - var suggestion = MetadataSuggestion.builder() // - .withId(MetadataSuggestion.NEW_ID) // - .withGeneration(ctx.getGeneration()) // - .withRecommender(ctx.getRecommender()) // - .withDocumentName(ctx.getDocument().getName()) // - .withLabel(label) // - .withUiLabel(label) // - .withScore(score) // - .withScoreExplanation(scoreExplanation) // - .withAutoAcceptMode(autoAcceptMode) // - .build(); - ctx.getResult().add(suggestion); - } - } - public static List extractSuggestions(int aGeneration, CAS aOriginalCas, CAS aPredictionCas, SourceDocument aDocument, Recommender aRecommender) { @@ -112,7 +55,7 @@ public static List extractSuggestions(int aGeneration, CAS break; } case DocumentMetadataLayerSupport.TYPE: { - extractDocumentMetadataSuggestion(ctx, predictedFS); + MetadataSuggestionSupport.extractSuggestion(ctx, predictedFS); break; } default: diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java index 4983e4acf3b..f2c36486dd5 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java @@ -76,9 +76,8 @@ 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.SuggestionGroup; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.ExtractionContext; import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderProperties; -import de.tudarmstadt.ukp.inception.recommendation.service.ExtractionContext; -import de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction; import de.tudarmstadt.ukp.inception.recommendation.util.OverlapIterator; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; @@ -463,10 +462,8 @@ public Optional getRenderer() public static void extractSuggestion(ExtractionContext ctx, TOP predictedFS) { - var autoAcceptMode = SuggestionExtraction.getAutoAcceptMode(predictedFS, - ctx.getModeFeature()); - var labels = SuggestionExtraction.getPredictedLabels(predictedFS, ctx.getLabelFeature(), - ctx.isMultiLabels()); + var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); + var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), ctx.isMultiLabels()); var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); 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 103889e24d2..7738e321921 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 @@ -45,7 +45,6 @@ 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.relation.RelationSuggestionSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @ExtendWith(MockitoExtension.class) diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java index 674f0410f8b..3f8f367c3fe 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java @@ -55,7 +55,6 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.Offset; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; -import de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @ExtendWith(MockitoExtension.class) From 352d176e514d53cef1ab63f808409690d5c5d315 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Mon, 25 Dec 2023 11:41:40 +0100 Subject: [PATCH 09/13] #2696 - Document-level recommendations - Simplify API and tests --- .../api/model/AnnotationSuggestion.java | 2 +- .../recommendation/api/model/Predictions.java | 5 + .../api/model/RelationSuggestion.java | 9 ++ .../api/model/SpanSuggestion.java | 47 ++++---- .../api/model/AnnotationSuggestionTest.java | 43 +++++--- .../api/model/PredictionGroupTest.java | 100 ++++++++++++------ .../api/model/PredictionsTest.java | 27 ++--- .../api/model/SpanSuggestionTest.java | 20 ++-- .../span/SpanSuggestionSupport.java | 2 +- .../recommendation/service/Fixtures.java | 51 ++++----- ...ommendationServiceImplIntegrationTest.java | 46 ++++---- .../RecommendationServiceImplTest.java | 14 +-- ...anSuggestionVisibilityCalculationTest.java | 63 +++++++---- 13 files changed, 253 insertions(+), 176 deletions(-) diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestion.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestion.java index e13dfb989c7..42cfc1c5946 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestion.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestion.java @@ -106,7 +106,7 @@ public AnnotationSuggestion(int aId, int aGeneration, int aAge, long aRecommende scoreExplanation = aScoreExplanation; recommenderId = aRecommenderId; documentName = aDocumentName; - autoAcceptMode = aAutoAcceptMode; + autoAcceptMode = aAutoAcceptMode != null ? aAutoAcceptMode : AutoAcceptMode.NEVER; hidingFlags = aHidingFlags; } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java index 14f0c17e38a..11488d772de 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java @@ -335,6 +335,11 @@ public List getPredictionsByRecommenderAndDocument( } } + public List getPredictionsByDocument(SourceDocument aDocument) + { + return getPredictionsByDocument(aDocument.getName()); + } + public List getPredictionsByDocument(String aDocumentName) { synchronized (predictionsLock) { 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 ca1a7d22730..46c1d0bffda 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 @@ -21,6 +21,8 @@ import org.apache.commons.lang3.builder.ToStringBuilder; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; + public class RelationSuggestion extends AnnotationSuggestion implements Serializable @@ -206,6 +208,13 @@ public Builder withFeature(String aFeature) return this; } + public Builder withDocument(SourceDocument aDocument) + { + this.documentName = aDocument.getName(); + return this; + } + + @Deprecated public Builder withDocumentName(String aDocumentName) { this.documentName = aDocumentName; diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SpanSuggestion.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SpanSuggestion.java index d6041865c59..3d5ba6aae1e 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SpanSuggestion.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SpanSuggestion.java @@ -21,6 +21,8 @@ import org.apache.commons.lang3.builder.ToStringBuilder; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; + public class SpanSuggestion extends AnnotationSuggestion implements Serializable @@ -41,22 +43,6 @@ private SpanSuggestion(Builder builder) coveredText = builder.coveredText; } - /** - * @deprecated Use builder instead. - */ - @Deprecated - public SpanSuggestion(int aId, long aRecommenderId, String aRecommenderName, long aLayerId, - String aFeature, String aDocumentName, int aBegin, int aEnd, String aCoveredText, - String aLabel, String aUiLabel, double aScore, String aScoreExplanation, - AutoAcceptMode aAutoAcceptMode) - { - super(aId, 0, 0, aRecommenderId, aRecommenderName, aLayerId, aFeature, aDocumentName, - aLabel, aUiLabel, aScore, aScoreExplanation, aAutoAcceptMode, 0); - - position = new Offset(aBegin, aEnd); - coveredText = aCoveredText; - } - // Getter and setter public String getCoveredText() @@ -164,7 +150,7 @@ public static final class Builder private String scoreExplanation; private Offset position; private String coveredText; - private AutoAcceptMode autoAcceptMode; + private AutoAcceptMode autoAcceptMode = AutoAcceptMode.NEVER; private int hidingFlags; private Builder() @@ -198,31 +184,42 @@ public Builder withRecommender(Recommender aRecommender) return this; } - public Builder withRecommenderId(long aRecommenderId) + @Deprecated + Builder withRecommenderId(long aRecommenderId) { this.recommenderId = aRecommenderId; return this; } - public Builder withRecommenderName(String aRecommenderName) + @Deprecated + Builder withRecommenderName(String aRecommenderName) { this.recommenderName = aRecommenderName; return this; } - public Builder withLayerId(long aLayerId) + @Deprecated + Builder withLayerId(long aLayerId) { this.layerId = aLayerId; return this; } - public Builder withFeature(String aFeature) + @Deprecated + Builder withFeature(String aFeature) { this.feature = aFeature; return this; } - public Builder withDocumentName(String aDocumentName) + public Builder withDocument(SourceDocument aDocument) + { + this.documentName = aDocument.getName(); + return this; + } + + @Deprecated + Builder withDocumentName(String aDocumentName) { this.documentName = aDocumentName; return this; @@ -252,6 +249,12 @@ public Builder withScoreExplanation(String aScoreExplanation) return this; } + public Builder withPosition(int aBegin, int aEnd) + { + this.position = new Offset(aBegin, aEnd); + return this; + } + public Builder withPosition(Offset aPosition) { this.position = aPosition; diff --git a/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestionTest.java b/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestionTest.java index 296d2ee85d9..db4fb5cf2de 100644 --- a/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestionTest.java +++ b/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/AnnotationSuggestionTest.java @@ -17,31 +17,44 @@ */ package de.tudarmstadt.ukp.inception.recommendation.api.model; -import static de.tudarmstadt.ukp.inception.recommendation.api.model.AutoAcceptMode.NEVER; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; -import java.util.List; - import org.junit.jupiter.api.Test; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; + public class AnnotationSuggestionTest { @Test public void thatEqualsAndHashCodeAndCompareToWorkCorrectly() { - SpanSuggestion rec1Sug1 = new SpanSuggestion(1, 1, "rec1", 1, "value", "doc1", 0, 1, "a", - "A", "#A", 0.1, "E1", NEVER); - SpanSuggestion rec1Sug2 = new SpanSuggestion(2, 1, "rec1", 1, "value", "doc1", 0, 1, "b", - "B", "#B", 0.2, "E2", NEVER); - SpanSuggestion rec2Sug1 = new SpanSuggestion(3, 2, "rec2", 1, "value", "doc1", 0, 1, "c", - "C", "#C", 0.1, "E1", NEVER); - SpanSuggestion rec2Sug2 = new SpanSuggestion(4, 2, "rec2", 1, "value", "doc1", 0, 1, "d", - "D", "#D", 0.3, "E3", NEVER); - - List all = asList(rec1Sug1, rec1Sug2, rec2Sug1, rec2Sug2); - for (SpanSuggestion x : all) { - for (SpanSuggestion y : all) { + var doc = SourceDocument.builder().withName("doc1").build(); + var layer = AnnotationLayer.builder().withId(1l).build(); + var feature = AnnotationFeature.builder().withLayer(layer).withName("value").build(); + var rec1 = Recommender.builder().withId(1l).withLayer(layer).withFeature(feature).build(); + var rec2 = Recommender.builder().withId(2l).withLayer(layer).withFeature(feature).build(); + + var builder = SpanSuggestion.builder().withLayerId(1).withFeature("value").withDocument(doc) + .withPosition(0, 1); + + builder.withRecommender(rec1); + var rec1Sug1 = builder.withId(1).withCoveredText("a").withLabel("A").withUiLabel("#A") + .withScore(0.1).withScoreExplanation("E1").build(); + var rec1Sug2 = builder.withId(2).withCoveredText("b").withLabel("B").withUiLabel("#B") + .withScore(0.2).withScoreExplanation("E2").build(); + + builder.withRecommender(rec2); + var rec2Sug1 = builder.withId(3).withCoveredText("c").withLabel("C").withUiLabel("#C") + .withScore(0.1).withScoreExplanation("E1").build(); + var rec2Sug2 = builder.withId(4).withCoveredText("d").withLabel("D").withUiLabel("#D") + .withScore(0.3).withScoreExplanation("E3").build(); + + var all = asList(rec1Sug1, rec1Sug2, rec2Sug1, rec2Sug2); + for (var x : all) { + for (var y : all) { if (x == y) { assertThat(x).isEqualTo(y); assertThat(y).isEqualTo(x); diff --git a/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/PredictionGroupTest.java b/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/PredictionGroupTest.java index 4dec3c7510b..67ed18f16b8 100644 --- a/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/PredictionGroupTest.java +++ b/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/PredictionGroupTest.java @@ -17,53 +17,82 @@ */ package de.tudarmstadt.ukp.inception.recommendation.api.model; -import static de.tudarmstadt.ukp.inception.recommendation.api.model.AutoAcceptMode.NEVER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; + public class PredictionGroupTest { + private SourceDocument doc; + private AnnotationLayer layer; + private AnnotationFeature feature; + private Recommender rec1; + private Recommender rec2; + + @BeforeEach + void setup() + { + doc = SourceDocument.builder().withId(123l).withName("doc1").build(); + layer = AnnotationLayer.builder().withId(1337l).withName("layer").build(); + feature = AnnotationFeature.builder().withId(1338l).withName("value").withLayer(layer) + .build(); + rec1 = Recommender.builder().withId(1l).withName("rec1").withLayer(layer) + .withFeature(feature).build(); + rec2 = Recommender.builder().withId(2l).withName("rec2").withLayer(layer) + .withFeature(feature).build(); + } + @Test public void thatAddingElementsToGroupWorks() { - var rec1Sug1 = new SpanSuggestion(1, 1, "rec1", 1, "value", "doc1", 0, 1, "a", "A", "#A", - 0.1, "E1", NEVER); - var rec1Sug2 = new SpanSuggestion(2, 1, "rec1", 1, "value", "doc1", 0, 1, "b", "B", "#B", - 0.2, "E2", NEVER); - var rec2Sug1 = new SpanSuggestion(3, 2, "rec2", 1, "value", "doc1", 0, 1, "c", "C", "#C", - 0.1, "E1", NEVER); - var rec2Sug2 = new SpanSuggestion(4, 2, "rec2", 1, "value", "doc1", 0, 1, "d", "D", "#D", - 0.3, "E3", NEVER); + var builder = SpanSuggestion.builder().withDocument(doc).withPosition(0, 1); + + builder.withRecommender(rec1); + var rec1Sug1 = builder.withId(1).withCoveredText("a").withLabel("A").withUiLabel("#A") + .withScore(0.1).withScoreExplanation("E1").build(); + var rec1Sug2 = builder.withId(2).withCoveredText("b").withLabel("B").withUiLabel("#B") + .withScore(0.2).withScoreExplanation("E2").build(); + + builder.withRecommender(rec2); + var rec2Sug1 = builder.withId(3).withCoveredText("c").withLabel("C").withUiLabel("#C") + .withScore(0.1).withScoreExplanation("E1").build(); + var rec2Sug2 = builder.withId(4).withCoveredText("d").withLabel("D").withUiLabel("#D") + .withScore(0.3).withScoreExplanation("E3").build(); // Ensure that group grows and that all elements are added properly var sut = new SuggestionGroup<>(); sut.add(rec1Sug1); - assertThat(sut).hasSize(1); - assertThat(sut).contains(rec1Sug1); + assertThat(sut).hasSize(1).contains(rec1Sug1); sut.add(rec1Sug2); - assertThat(sut).hasSize(2); - assertThat(sut).contains(rec1Sug2); + assertThat(sut).hasSize(2).contains(rec1Sug2); sut.add(rec2Sug1); - assertThat(sut).hasSize(3); - assertThat(sut).contains(rec2Sug1); + assertThat(sut).hasSize(3).contains(rec2Sug1); sut.add(rec2Sug2); - assertThat(sut).hasSize(4); - assertThat(sut).contains(rec2Sug2); + assertThat(sut).hasSize(4).contains(rec2Sug2); } @Test public void thatSortingWorks() { - var rec1Sug1 = new SpanSuggestion(1, 1, "rec1", 1, "value", "doc1", 0, 1, "a", "A", "#A", - 0.1, "E1", NEVER); - var rec1Sug2 = new SpanSuggestion(2, 1, "rec1", 1, "value", "doc1", 0, 1, "b", "B", "#B", - 0.2, "E2", NEVER); - var rec2Sug1 = new SpanSuggestion(3, 2, "rec2", 1, "value", "doc1", 0, 1, "c", "C", "#C", - 0.1, "E1", NEVER); - var rec2Sug2 = new SpanSuggestion(4, 2, "rec2", 1, "value", "doc1", 0, 1, "d", "D", "#D", - 0.3, "E3", NEVER); + var builder = SpanSuggestion.builder().withDocument(doc).withPosition(0, 1); + + builder.withRecommender(rec1); + var rec1Sug1 = builder.withId(1).withCoveredText("a").withLabel("A").withUiLabel("#A") + .withScore(0.1).withScoreExplanation("E1").build(); + var rec1Sug2 = builder.withId(2).withCoveredText("b").withLabel("B").withUiLabel("#B") + .withScore(0.2).withScoreExplanation("E2").build(); + + builder.withRecommender(rec2); + var rec2Sug1 = builder.withId(3).withCoveredText("c").withLabel("C").withUiLabel("#C") + .withScore(0.1).withScoreExplanation("E1").build(); + var rec2Sug2 = builder.withId(4).withCoveredText("d").withLabel("D").withUiLabel("#D") + .withScore(0.3).withScoreExplanation("E3").build(); var sut = new SuggestionGroup<>(rec1Sug1, rec1Sug2, rec2Sug1, rec2Sug2); @@ -83,14 +112,19 @@ public void thatSortingWorks() @Test public void thatTopDeltasAreCorrect() { - var rec1Sug1 = new SpanSuggestion(1, 1, "rec1", 1, "value", "doc1", 0, 1, "a", "A", "#A", - 0.1, "E1", NEVER); - var rec1Sug2 = new SpanSuggestion(2, 1, "rec1", 1, "value", "doc1", 0, 1, "b", "B", "#B", - 0.2, "E2", NEVER); - var rec2Sug1 = new SpanSuggestion(3, 2, "rec2", 1, "value", "doc1", 0, 1, "c", "C", "#C", - 0.1, "E1", NEVER); - var rec2Sug2 = new SpanSuggestion(4, 2, "rec2", 1, "value", "doc1", 0, 1, "d", "D", "#D", - 0.3, "E3", NEVER); + var builder = SpanSuggestion.builder().withDocument(doc).withPosition(0, 1); + + builder.withRecommender(rec1); + var rec1Sug1 = builder.withId(1).withCoveredText("a").withLabel("A").withUiLabel("#A") + .withScore(0.1).withScoreExplanation("E1").build(); + var rec1Sug2 = builder.withId(2).withCoveredText("b").withLabel("B").withUiLabel("#B") + .withScore(0.2).withScoreExplanation("E2").build(); + + builder.withRecommender(rec2); + var rec2Sug1 = builder.withId(3).withCoveredText("c").withLabel("C").withUiLabel("#C") + .withScore(0.1).withScoreExplanation("E1").build(); + var rec2Sug2 = builder.withId(4).withCoveredText("d").withLabel("D").withUiLabel("#D") + .withScore(0.3).withScoreExplanation("E3").build(); var sut = new SuggestionGroup<>(rec1Sug1, rec1Sug2, rec2Sug1, rec2Sug2); diff --git a/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/PredictionsTest.java b/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/PredictionsTest.java index c34b608b3aa..9dfa4621b10 100644 --- a/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/PredictionsTest.java +++ b/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/PredictionsTest.java @@ -37,8 +37,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +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.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Token; @@ -112,12 +114,12 @@ void timeGetGroupedPredictions() throws Exception @Test void thatIdsAreAssigned() throws Exception { - var doc = "doc"; + var doc = SourceDocument.builder().withName("doc").build(); sut = new Predictions(user, user.getUsername(), project); sut.putPredictions(asList( // SpanSuggestion.builder() // .withId(AnnotationSuggestion.NEW_ID) // - .withDocumentName(doc) // + .withDocument(doc) // .build())); assertThat(sut.getPredictionsByDocument(doc)) // @@ -129,7 +131,7 @@ void thatIdsAreAssigned() throws Exception sut.putPredictions(asList( // SpanSuggestion.builder() // .withId(AnnotationSuggestion.NEW_ID) // - .withDocumentName(doc) // + .withDocument(doc) // .build())); sut.putPredictions(inheritedPredictions); @@ -148,25 +150,24 @@ private List generatePredictions(int aDocs, int aRecommend var tokens = cas.select(Token.class).asList(); var result = new ArrayList(); - for (int docId = 0; docId < aDocs; docId++) { - var docName = "doc" + docId; - for (var recId = 0; recId < aRecommenders; recId++) { - var recName = "rec" + recId; - var featName = "feat" + recId; + for (var docId = 0l; docId < aDocs; docId++) { + var doc = SourceDocument.builder().withId(docId).withName("doc" + docId).build(); + for (var recId = 0l; recId < aRecommenders; recId++) { + var feature = AnnotationFeature.builder().withId(recId).withName("feat" + recId) + .build(); + var rec = Recommender.builder().withId(recId).withName("rec" + recId) + .withLayer(layer).withFeature(feature).build(); for (int annId = 0; annId < aSuggestions; annId++) { var label = labels.get(rng.nextInt(labels.size())); var token = tokens.get(rng.nextInt(tokens.size())); var ann = SpanSuggestion.builder() // .withId(annId) // - .withDocumentName(docName) // - .withRecommenderName(recName) // - .withRecommenderId(recId) // - .withLayerId(layer.getId()) // + .withDocument(doc) // + .withRecommender(rec) // .withScore(rng.nextDouble()) // .withScoreExplanation(null) // .withLabel(label) // .withUiLabel(label) // - .withFeature(featName) // .withPosition(new Offset(token.getBegin(), token.getEnd())) // .withCoveredText(token.getCoveredText()) // .build(); diff --git a/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SpanSuggestionTest.java b/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SpanSuggestionTest.java index e21c1cb7ff5..bc8eeb1386b 100644 --- a/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SpanSuggestionTest.java +++ b/inception/inception-recommendation-api/src/test/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SpanSuggestionTest.java @@ -21,25 +21,33 @@ import org.junit.jupiter.api.Test; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature; +import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; + class SpanSuggestionTest { @Test void thatCloningRetainsAllInformation() { + var layer = AnnotationLayer.builder().withId(5l).withName("layer").build(); + var feature = AnnotationFeature.builder().withId(6l).withName("feature").withLayer(layer) + .build(); + var rec = Recommender.builder().withId(4l).withName("rec").withLayer(layer) + .withFeature(feature).build(); + var doc = SourceDocument.builder().withId(8l).withName("doc").build(); + var s = SpanSuggestion.builder() // .withId(1) // .withGeneration(2) // .withAge(3) // - .withRecommenderId(4) // - .withRecommenderName("rec") // - .withLayerId(5) // - .withFeature("feature") // - .withDocumentName("document") // + .withRecommender(rec) // + .withDocument(doc) // .withLabel("label") // .withUiLabel("uiLabel") // .withScore(6.0) // .withScoreExplanation("scoreExplanation") // - .withPosition(new Offset(1, 2)) // + .withPosition(1, 2) // .withCoveredText("coveredText") // .withAutoAcceptMode(AutoAcceptMode.ON_FIRST_ACCESS) // .withHidingFlags(7) // diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java index f2c36486dd5..10869bfeb86 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java @@ -485,7 +485,7 @@ public static void extractSuggestion(ExtractionContext ctx, TOP predictedFS) .withId(RelationSuggestion.NEW_ID) // .withGeneration(ctx.getGeneration()) // .withRecommender(ctx.getRecommender()) // - .withDocumentName(ctx.getDocument().getName()) // + .withDocument(ctx.getDocument()) // .withPosition(offsets) // .withCoveredText(coveredText) // .withLabel(label) // 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 56201f6b50a..5719102c648 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 @@ -17,17 +17,15 @@ */ package de.tudarmstadt.ukp.inception.recommendation.service; -import static de.tudarmstadt.ukp.inception.recommendation.api.model.AutoAcceptMode.NEVER; -import static java.util.stream.Collectors.toList; - import java.util.ArrayList; import java.util.Collection; import java.util.List; 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.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; @@ -38,7 +36,6 @@ public class Fixtures // AnnotationSuggestion private final static long RECOMMENDER_ID = 1; private final static String RECOMMENDER_NAME = "TestEntityRecommender"; - private final static String UI_LABEL = "TestUiLabel"; private final static double CONFIDENCE = 0.2; private final static String CONFIDENCE_EXPLANATION = "Predictor A: 0.05 | Predictor B: 0.15"; private final static String COVERED_TEXT = "TestText"; @@ -49,7 +46,7 @@ public static List getInvisibleSuggestions( return aSuggestions.stream() // .flatMap(SuggestionGroup::stream) // .filter(s -> !s.isVisible()) // - .collect(toList()); + .toList(); } public static List getVisibleSuggestions( @@ -58,17 +55,21 @@ public static List getVisibleSuggestions( return aSuggestions.stream() // .flatMap(SuggestionGroup::stream) // .filter(s -> s.isVisible()) // - .collect(toList()); + .toList(); } public static SuggestionDocumentGroup makeSpanSuggestionGroup( 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) { - suggestions.add(new SpanSuggestion(val[0], RECOMMENDER_ID, RECOMMENDER_NAME, - aFeat.getLayer().getId(), aFeat.getName(), doc.getName(), val[1], val[2], - COVERED_TEXT, null, UI_LABEL, CONFIDENCE, CONFIDENCE_EXPLANATION, NEVER)); + var suggestion = SpanSuggestion.builder().withId(val[0]).withRecommender(rec) + .withDocument(doc).withPosition(val[1], val[2]).withCoveredText(COVERED_TEXT) + .withScore(CONFIDENCE).withScoreExplanation(CONFIDENCE_EXPLANATION).build(); + suggestions.add(suggestion); } return new SuggestionDocumentGroup<>(suggestions); @@ -79,31 +80,15 @@ public static SuggestionDocumentGroup makeRelationSuggestion { List suggestions = new ArrayList<>(); for (int[] val : vals) { - suggestions.add(new RelationSuggestion(val[0], RECOMMENDER_ID, RECOMMENDER_NAME, - aFeat.getLayer().getId(), aFeat.getName(), doc.getName(), val[1], val[2], - val[3], val[4], null, UI_LABEL, CONFIDENCE, CONFIDENCE_EXPLANATION, NEVER)); + var suggestion = RelationSuggestion.builder().withId(val[0]) + .withRecommenderId(RECOMMENDER_ID).withRecommenderName(RECOMMENDER_NAME) + .withFeature(aFeat.getName()).withLayerId(aFeat.getLayer().getId()) + .withDocument(doc) + .withPosition(new RelationPosition(val[1], val[2], val[3], val[4])) + .withScore(CONFIDENCE).withScoreExplanation(CONFIDENCE_EXPLANATION).build(); + suggestions.add(suggestion); } return new SuggestionDocumentGroup<>(suggestions); } - - public static SpanSuggestion makeSuggestion(int aBegin, int aEnd, String aLabel, - SourceDocument aDoc, AnnotationLayer aLayer, AnnotationFeature aFeature) - { - return new SpanSuggestion(0, // aId, - 0, // aRecommenderId, - "", // aRecommenderName - aLayer.getId(), // aLayerId, - aFeature.getName(), // aFeature, - aDoc.getName(), // aDocumentName - aBegin, // aBegin - aEnd, // aEnd - "", // aCoveredText, - aLabel, // aLabel - aLabel, // aUiLabel - 0.0, // aScore - "", // aScoreExplanation, - NEVER // autoAccept - ); - } } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java index 9609e383c55..bcafa0d907b 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java @@ -354,21 +354,19 @@ public void thatSpanSuggestionsCanBeRecorded() var sourceDoc = createSourceDocument("doc"); var layer = createAnnotationLayer("layer"); var feature = createAnnotationFeature(layer, FEATURE_NAME); + var rec = Recommender.builder().withId(1337l).withName("testRecommender").withLayer(layer) + .withFeature(feature).build(); var suggestion = SpanSuggestion.builder() // .withId(42) // - .withRecommenderId(1337) // - .withRecommenderName("testRecommender") // - .withLayerId(layer.getId()) // - .withFeature(feature.getName()) // - .withDocumentName(sourceDoc.getName()) // - .withPosition(new Offset(7, 14)) // + .withRecommender(rec) // + .withDocument(sourceDoc) // + .withPosition(7, 14) // .withCoveredText("aCoveredText") // .withLabel("testLabel") // .withUiLabel("testUiLabel") // .withScore(0.42) // .withScoreExplanation("Test confidence") // - .withAutoAcceptMode(NEVER) // .build(); sut.logRecord(USER_NAME, sourceDoc, USER_NAME, suggestion, feature, ACCEPTED, MAIN_EDITOR); @@ -437,28 +435,34 @@ void thatListingRecordsForRendering() var feature1 = createAnnotationFeature(layer1, "feat1"); var feature2 = createAnnotationFeature(layer2, "feat1"); + var rec1 = Recommender.builder().withId(1337l).withName("testRecommender").withLayer(layer1) + .withFeature(feature1).build(); + var rec2 = Recommender.builder().withId(1337l).withName("testRecommender2") + .withLayer(layer2).withFeature(feature2).build(); + + Offset position = new Offset(7, 14); sut.logRecord(USER_NAME, sourceDoc1, USER_NAME, - new SpanSuggestion(42, 1337, "testRecommender", layer1.getId(), feature1.getName(), - sourceDoc1.getName(), 7, 14, "aCoveredText", "testLabel", "testUiLabel", - 0.42, "Test confidence", NEVER), + SpanSuggestion.builder().withRecommender(rec1).withDocument(sourceDoc1) + .withPosition(position).withLabel("testLabel") + .withCoveredText("aCoveredText").build(), feature1, ACCEPTED, MAIN_EDITOR); sut.logRecord(USER_NAME, sourceDoc1, USER_NAME, - new SpanSuggestion(42, 1337, "testRecommender2", layer2.getId(), feature2.getName(), - sourceDoc1.getName(), 7, 14, "aCoveredText", "testLabel", "testUiLabel", - 0.42, "Test confidence", NEVER), + SpanSuggestion.builder().withRecommender(rec2).withDocument(sourceDoc1) + .withPosition(position).withLabel("testLabel") + .withCoveredText("aCoveredText").build(), feature2, ACCEPTED, MAIN_EDITOR); sut.logRecord(USER_NAME, sourceDoc2, USER_NAME, - new SpanSuggestion(42, 1337, "testRecommender", layer1.getId(), feature1.getName(), - sourceDoc2.getName(), 7, 14, "aCoveredText", "testLabel", "testUiLabel", - 0.42, "Test confidence", NEVER), + SpanSuggestion.builder().withRecommender(rec1).withDocument(sourceDoc1) + .withPosition(position).withLabel("testLabel") + .withCoveredText("aCoveredText").build(), feature1, ACCEPTED, MAIN_EDITOR); sut.logRecord(USER_NAME, sourceDoc2, USER_NAME, - new SpanSuggestion(42, 1337, "testRecommender2", layer2.getId(), feature2.getName(), - sourceDoc2.getName(), 7, 14, "aCoveredText", "testLabel", "testUiLabel", - 0.42, "Test confidence", NEVER), + SpanSuggestion.builder().withRecommender(rec2).withDocument(sourceDoc1) + .withPosition(position).withLabel("testLabel") + .withCoveredText("aCoveredText").build(), feature2, ACCEPTED, MAIN_EDITOR); assertThat(sut.listLearningRecords(USER_NAME, sourceDoc1, USER_NAME, feature1)).hasSize(1); @@ -473,9 +477,7 @@ void thatListingRecordsForRendering() private SourceDocument createSourceDocument(String aName) { - var doc = new SourceDocument(); - doc.setProject(project); - doc.setName(aName); + var doc = SourceDocument.builder().withProject(project).withName(aName).build(); return testEntityManager.persist(doc); } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java index f4c65e5a99a..e8132f4ec91 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java @@ -90,15 +90,15 @@ void testReconciliation() throws Exception var existingSuggestions = Arrays. asList( // SpanSuggestion.builder() // .withId(0) // - .withPosition(new Offset(0, 10)) // - .withDocumentName(doc.getName()) // + .withPosition(0, 10) // + .withDocument(doc) // .withLabel("aged") // .withRecommender(rec) // .build(), SpanSuggestion.builder() // .withId(1) // - .withPosition(new Offset(0, 10)) // - .withDocumentName(doc.getName()) // + .withPosition(0, 10) // + .withDocument(doc) // .withLabel("removed") // .withRecommender(rec) // .build()); @@ -108,15 +108,15 @@ void testReconciliation() throws Exception var newSuggestions = Arrays. asList( // SpanSuggestion.builder() // .withId(2) // - .withPosition(new Offset(0, 10)) // - .withDocumentName(doc.getName()) // + .withPosition(0, 10) // + .withDocument(doc) // .withLabel("aged") // .withRecommender(rec) // .build(), SpanSuggestion.builder() // .withId(3) // .withPosition(new Offset(0, 10)) // - .withDocumentName(doc.getName()) // + .withDocument(doc) // .withLabel("added") // .withRecommender(rec) // .build()); diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java index 3f8f367c3fe..1d4ca843425 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java @@ -22,9 +22,9 @@ 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.makeSpanSuggestionGroup; -import static de.tudarmstadt.ukp.inception.recommendation.service.Fixtures.makeSuggestion; import static de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport.hideSuggestionsRejectedOrSkipped; import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; import static org.apache.uima.cas.CAS.TYPE_NAME_STRING; import static org.assertj.core.api.Assertions.assertThat; @@ -52,7 +52,7 @@ 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.LearningRecordUserAction; -import de.tudarmstadt.ukp.inception.recommendation.api.model.Offset; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; @@ -212,28 +212,28 @@ public void thatVisibilityIsRestoredWhenOverlappingAnnotationIsRemoved() throws @Test public void thatOverlappingSuggestionsAreNotHiddenWhenStackingIsEnabled() throws Exception { - doReturn(new ArrayList<>()).when(learningRecordService).listLearningRecords(TEST_USER, - TEST_USER, layer); + doReturn(emptyList()).when(learningRecordService).listLearningRecords(TEST_USER, TEST_USER, + layer); layer.setOverlapMode(OverlapMode.ANY_OVERLAP); + var rec = Recommender.builder().withId(123l).withName("rec").withLayer(layer) + .withFeature(feature).build(); var cas = JCasFactory.createText("a b", "de"); var suggestion1 = SpanSuggestion.builder() // .withId(1) // - .withDocumentName(doc.getName()) // - .withLayerId(layer.getId()) // - .withFeature(NamedEntity._FeatName_value) // + .withDocument(doc) // + .withRecommender(rec) // .withLabel("blah") // - .withPosition(new Offset(0, 1)) // + .withPosition(0, 1) // .build(); var suggestion2 = SpanSuggestion.builder() // .withId(2) // - .withDocumentName(doc.getName()) // - .withLayerId(layer.getId()) // - .withFeature(NamedEntity._FeatName_value) // + .withDocument(doc) // + .withRecommender(rec) // .withLabel("blah") // - .withPosition(new Offset(1, 2)) // + .withPosition(1, 2) // .build(); var suggestions = new SuggestionDocumentGroup<>(asList(suggestion1, suggestion2)); @@ -278,17 +278,23 @@ public void thatOverlappingSuggestionsAreNotHiddenWhenStackingIsEnabled() throws @Test void thatRejectedSuggestionIsHidden() { + var rec1 = Recommender.builder().withId(1l).withLayer(layer).withFeature(feature).build(); + var rec2 = Recommender.builder().withId(1l).withLayer(layer2).withFeature(feature).build(); + var rec3 = Recommender.builder().withId(1l).withLayer(layer).withFeature(feature2).build(); + var label = "x"; + var records = asList(LearningRecord.builder() // .withSourceDocument(doc) // .withLayer(layer) // .withAnnotationFeature(feature) // .withOffsetBegin(0) // .withOffsetEnd(10) // - .withAnnotation("x") // + .withAnnotation(label) // .withUserAction(REJECTED) // .build()); - var docSuggestion = makeSuggestion(0, 10, "x", doc, layer, feature); + var docSuggestion = SpanSuggestion.builder().withRecommender(rec1).withDocument(doc) + .withLabel(label).withPosition(0, 10).build(); assertThat(docSuggestion.isVisible()).isTrue(); hideSuggestionsRejectedOrSkipped(docSuggestion, records); assertThat(docSuggestion.isVisible()) // @@ -296,21 +302,24 @@ void thatRejectedSuggestionIsHidden() .isFalse(); assertThat(docSuggestion.getReasonForHiding().trim()).isEqualTo("rejected"); - var doc2Suggestion = makeSuggestion(0, 10, "x", doc2, layer, feature); + var doc2Suggestion = SpanSuggestion.builder().withRecommender(rec1).withDocument(doc2) + .withLabel(label).withPosition(0, 10).build(); assertThat(doc2Suggestion.isVisible()).isTrue(); hideSuggestionsRejectedOrSkipped(doc2Suggestion, records); assertThat(doc2Suggestion.isVisible()) // .as("Suggestion in other document should not be hidden") // .isTrue(); - var doc3Suggestion = makeSuggestion(0, 10, "x", doc, layer2, feature); + var doc3Suggestion = SpanSuggestion.builder().withRecommender(rec2).withDocument(doc) + .withLabel(label).withPosition(0, 10).build(); assertThat(doc3Suggestion.isVisible()).isTrue(); hideSuggestionsRejectedOrSkipped(doc3Suggestion, records); assertThat(doc3Suggestion.isVisible()) // .as("Suggestion in other layer should not be hidden") // .isTrue(); - var doc4Suggestion = makeSuggestion(0, 10, "x", doc, layer, feature2); + var doc4Suggestion = SpanSuggestion.builder().withRecommender(rec3).withDocument(doc) + .withLabel(label).withPosition(0, 10).build(); assertThat(doc4Suggestion.isVisible()).isTrue(); hideSuggestionsRejectedOrSkipped(doc3Suggestion, records); assertThat(doc4Suggestion.isVisible()) // @@ -321,17 +330,23 @@ void thatRejectedSuggestionIsHidden() @Test void thatSkippedSuggestionIsHidden() { + var rec1 = Recommender.builder().withId(1l).withLayer(layer).withFeature(feature).build(); + var rec2 = Recommender.builder().withId(1l).withLayer(layer2).withFeature(feature).build(); + var rec3 = Recommender.builder().withId(1l).withLayer(layer).withFeature(feature2).build(); + var label = "x"; + var records = asList(LearningRecord.builder() // .withSourceDocument(doc) // .withLayer(layer) // .withAnnotationFeature(feature) // .withOffsetBegin(0) // .withOffsetEnd(10) // - .withAnnotation("x") // + .withAnnotation(label) // .withUserAction(SKIPPED) // .build()); - var docSuggestion = makeSuggestion(0, 10, "x", doc, layer, feature); + var docSuggestion = SpanSuggestion.builder().withRecommender(rec1).withDocument(doc) + .withLabel(label).withPosition(0, 10).build(); assertThat(docSuggestion.isVisible()).isTrue(); hideSuggestionsRejectedOrSkipped(docSuggestion, records); assertThat(docSuggestion.isVisible()) // @@ -339,21 +354,23 @@ void thatSkippedSuggestionIsHidden() .isFalse(); assertThat(docSuggestion.getReasonForHiding().trim()).isEqualTo("skipped"); - var doc2Suggestion = makeSuggestion(0, 10, "x", doc2, layer, feature); + var doc2Suggestion = SpanSuggestion.builder().withRecommender(rec1).withDocument(doc2) + .withLabel(label).withPosition(0, 10).build(); assertThat(doc2Suggestion.isVisible()).isTrue(); hideSuggestionsRejectedOrSkipped(doc2Suggestion, records); assertThat(doc2Suggestion.isVisible()) // .as("Suggestion in other document should not be hidden") // .isTrue(); - var doc3Suggestion = makeSuggestion(0, 10, "x", doc, layer2, feature); - assertThat(doc3Suggestion.isVisible()).isTrue(); + var doc3Suggestion = SpanSuggestion.builder().withRecommender(rec2).withDocument(doc) + .withLabel(label).withPosition(0, 10).build(); hideSuggestionsRejectedOrSkipped(doc3Suggestion, records); assertThat(doc3Suggestion.isVisible()) // .as("Suggestion in other layer should not be hidden") // .isTrue(); - var doc4Suggestion = makeSuggestion(0, 10, "x", doc, layer, feature2); + var doc4Suggestion = SpanSuggestion.builder().withRecommender(rec3).withDocument(doc) + .withLabel(label).withPosition(0, 10).build(); assertThat(doc4Suggestion.isVisible()).isTrue(); hideSuggestionsRejectedOrSkipped(doc3Suggestion, records); assertThat(doc4Suggestion.isVisible()) // From e790b90a628a3bca45549566c48ee3155946f642 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Mon, 25 Dec 2023 12:13:47 +0100 Subject: [PATCH 10/13] #2696 - Document-level recommendations - Simplify API and tests --- .../sidebar/MetadataSuggestionSupport.java | 2 +- .../api/model/MetadataSuggestion.java | 23 +++++++++++---- .../api/model/RelationSuggestion.java | 28 ++++++------------- .../recommendation/service/Fixtures.java | 7 +++-- ...ommendationServiceImplIntegrationTest.java | 11 +++++--- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java index c15e30476f9..5175775f7a6 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java @@ -292,7 +292,7 @@ public static void extractSuggestion(ExtractionContext ctx, TOP predictedFS) .withId(MetadataSuggestion.NEW_ID) // .withGeneration(ctx.getGeneration()) // .withRecommender(ctx.getRecommender()) // - .withDocumentName(ctx.getDocument().getName()) // + .withDocument(ctx.getDocument()) // .withLabel(label) // .withUiLabel(label) // .withScore(score) // diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/MetadataSuggestion.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/MetadataSuggestion.java index 7bc98f300cc..90219ba92df 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/MetadataSuggestion.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/MetadataSuggestion.java @@ -19,6 +19,8 @@ import java.io.Serializable; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; + public class MetadataSuggestion extends AnnotationSuggestion implements Serializable @@ -129,31 +131,42 @@ public Builder withRecommender(Recommender aRecommender) return this; } - public Builder withRecommenderId(long aRecommenderId) + @Deprecated + Builder withRecommenderId(long aRecommenderId) { this.recommenderId = aRecommenderId; return this; } - public Builder withRecommenderName(String aRecommenderName) + @Deprecated + Builder withRecommenderName(String aRecommenderName) { this.recommenderName = aRecommenderName; return this; } - public Builder withLayerId(long aLayerId) + @Deprecated + Builder withLayerId(long aLayerId) { this.layerId = aLayerId; return this; } - public Builder withFeature(String aFeature) + @Deprecated + Builder withFeature(String aFeature) { this.feature = aFeature; return this; } - public Builder withDocumentName(String aDocumentName) + public Builder withDocument(SourceDocument aDocument) + { + this.documentName = aDocument.getName(); + return this; + } + + @Deprecated + Builder withDocumentName(String aDocumentName) { this.documentName = aDocumentName; return this; 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 46c1d0bffda..bf4602f917d 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 @@ -41,21 +41,6 @@ private RelationSuggestion(Builder builder) this.position = builder.position; } - /** - * @deprecated Use builder instead - */ - @Deprecated - public RelationSuggestion(int aId, long aRecommenderId, String aRecommenderName, long aLayerId, - String aFeature, String aDocumentName, int aSourceBegin, int aSourceEnd, - int aTargetBegin, int aTargetEnd, String aLabel, String aUiLabel, double aScore, - String aScoreExplanation, AutoAcceptMode aAutoAcceptMode) - { - super(aId, 0, 0, aRecommenderId, aRecommenderName, aLayerId, aFeature, aDocumentName, - aLabel, aUiLabel, aScore, aScoreExplanation, aAutoAcceptMode, 0); - - position = new RelationPosition(aSourceBegin, aSourceEnd, aTargetBegin, aTargetEnd); - } - // Getter and setter @Override @@ -184,25 +169,29 @@ public Builder withRecommender(Recommender aRecommender) return this; } - public Builder withRecommenderId(long aRecommenderId) + @Deprecated + Builder withRecommenderId(long aRecommenderId) { this.recommenderId = aRecommenderId; return this; } - public Builder withRecommenderName(String aRecommenderName) + @Deprecated + Builder withRecommenderName(String aRecommenderName) { this.recommenderName = aRecommenderName; return this; } - public Builder withLayerId(long aLayerId) + @Deprecated + Builder withLayerId(long aLayerId) { this.layerId = aLayerId; return this; } - public Builder withFeature(String aFeature) + @Deprecated + Builder withFeature(String aFeature) { this.feature = aFeature; return this; @@ -268,5 +257,4 @@ public RelationSuggestion build() return new RelationSuggestion(this); } } - } 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 5719102c648..c1be4b712cd 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 @@ -78,11 +78,12 @@ public static SuggestionDocumentGroup makeSpanSuggestionGroup( 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]) - .withRecommenderId(RECOMMENDER_ID).withRecommenderName(RECOMMENDER_NAME) - .withFeature(aFeat.getName()).withLayerId(aFeat.getLayer().getId()) + 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(); diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java index bcafa0d907b..8cdba07b2d5 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java @@ -23,7 +23,6 @@ import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; 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 de.tudarmstadt.ukp.inception.recommendation.api.model.AutoAcceptMode.NEVER; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation.DETAIL_EDITOR; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordChangeLocation.MAIN_EDITOR; import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction.ACCEPTED; @@ -73,6 +72,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction; import de.tudarmstadt.ukp.inception.recommendation.api.model.Offset; 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.recommender.RecommendationEngineFactory; @@ -397,10 +397,13 @@ public void thatRelationSuggestionsCanBeRecorded() var sourceDoc = createSourceDocument("doc"); var layer = createAnnotationLayer("layer"); var feature = createAnnotationFeature(layer, FEATURE_NAME); + var rec = Recommender.builder().withId(1337l).withName("testRecommender").withLayer(layer) + .withFeature(feature).build(); - var suggestion = new RelationSuggestion(42, 1337, "testRecommender", layer.getId(), - feature.getName(), sourceDoc.getName(), 7, 14, 21, 28, "testLabel", "testUiLabel", - 0.42, "Test confidence", NEVER); + var suggestion = RelationSuggestion.builder().withId(42).withRecommender(rec) + .withDocument(sourceDoc).withPosition(new RelationPosition(7, 14, 21, 28)) + .withLabel("testLabel").withUiLabel("testUiLabel").withScore(0.42) + .withScoreExplanation("Test confidence").build(); sut.logRecord(USER_NAME, sourceDoc, USER_NAME, suggestion, feature, LearningRecordUserAction.REJECTED, DETAIL_EDITOR); From afb315d0b4bf5f06149ed1db1f6f9e789dfa5982 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Mon, 25 Dec 2023 13:10:55 +0100 Subject: [PATCH 11/13] #2696 - Document-level recommendations - Towards handling extraction as part of the suggestion supports --- .../sidebar/MetadataSuggestionSupport.java | 49 +++++++----- .../api/recommender/ExtractionContext.java | 29 ------- .../relation/RelationSuggestionSupport.java | 75 +++++++++++-------- .../service/SuggestionExtraction.java | 37 ++++----- .../span/SpanSuggestionSupport.java | 72 +++++++++--------- 5 files changed, 123 insertions(+), 139 deletions(-) diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java index 5175775f7a6..70c2aad4103 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java @@ -23,6 +23,7 @@ import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction.REJECTED; import java.lang.invoke.MethodHandles; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -33,7 +34,6 @@ import org.apache.uima.cas.CAS; import org.apache.uima.cas.Feature; import org.apache.uima.jcas.cas.AnnotationBase; -import org.apache.uima.jcas.cas.TOP; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEventPublisher; @@ -280,26 +280,35 @@ public Optional getRenderer() return Optional.empty(); } - public static void extractSuggestion(ExtractionContext ctx, TOP predictedFS) + public static List extractSuggestions(ExtractionContext ctx) { - var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); - var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), ctx.isMultiLabels()); - var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); - var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); - - for (var label : labels) { - var suggestion = MetadataSuggestion.builder() // - .withId(MetadataSuggestion.NEW_ID) // - .withGeneration(ctx.getGeneration()) // - .withRecommender(ctx.getRecommender()) // - .withDocument(ctx.getDocument()) // - .withLabel(label) // - .withUiLabel(label) // - .withScore(score) // - .withScoreExplanation(scoreExplanation) // - .withAutoAcceptMode(autoAcceptMode) // - .build(); - ctx.getResult().add(suggestion); + var result = new ArrayList(); + for (var predictedFS : ctx.getPredictionCas().select(ctx.getPredictedType())) { + if (!predictedFS.getBooleanValue(ctx.getPredictionFeature())) { + continue; + } + + var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); + var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), + ctx.isMultiLabels()); + var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); + var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); + + for (var label : labels) { + var suggestion = MetadataSuggestion.builder() // + .withId(MetadataSuggestion.NEW_ID) // + .withGeneration(ctx.getGeneration()) // + .withRecommender(ctx.getRecommender()) // + .withDocument(ctx.getDocument()) // + .withLabel(label) // + .withUiLabel(label) // + .withScore(score) // + .withScoreExplanation(scoreExplanation) // + .withAutoAcceptMode(autoAcceptMode) // + .build(); + result.add(suggestion); + } } + return result; } } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/ExtractionContext.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/ExtractionContext.java index 58777e21480..6d333fc1889 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/ExtractionContext.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/ExtractionContext.java @@ -21,13 +21,8 @@ import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; 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 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.CAS.TYPE_NAME_STRING_ARRAY; -import java.util.ArrayList; -import java.util.List; - import org.apache.uima.cas.CAS; import org.apache.uima.cas.Feature; import org.apache.uima.cas.Type; @@ -35,7 +30,6 @@ import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer; 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; public final class ExtractionContext @@ -56,8 +50,6 @@ public final class ExtractionContext private final Type predictedType; private final Feature labelFeature; - private final Feature sourceFeature; - private final Feature targetFeature; private final Feature scoreFeature; private final Feature scoreExplanationFeature; private final Feature modeFeature; @@ -65,8 +57,6 @@ public final class ExtractionContext private final boolean isMultiLabels; - private final List result; - public ExtractionContext(int aGeneration, Recommender aRecommender, SourceDocument aDocument, CAS aOriginalCas, CAS aPredictionCas) { @@ -84,8 +74,6 @@ public ExtractionContext(int aGeneration, Recommender aRecommender, SourceDocume predictedType = CasUtil.getType(aPredictionCas, typeName); labelFeature = predictedType.getFeatureByBaseName(featureName); - sourceFeature = predictedType.getFeatureByBaseName(FEAT_REL_SOURCE); - targetFeature = predictedType.getFeatureByBaseName(FEAT_REL_TARGET); scoreFeature = predictedType.getFeatureByBaseName(featureName + FEATURE_NAME_SCORE_SUFFIX); scoreExplanationFeature = predictedType .getFeatureByBaseName(featureName + FEATURE_NAME_SCORE_EXPLANATION_SUFFIX); @@ -93,8 +81,6 @@ public ExtractionContext(int aGeneration, Recommender aRecommender, SourceDocume .getFeatureByBaseName(featureName + FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX); predictionFeature = predictedType.getFeatureByBaseName(FEATURE_NAME_IS_PREDICTION); isMultiLabels = TYPE_NAME_STRING_ARRAY.equals(labelFeature.getRange().getName()); - - result = new ArrayList(); } public int getGeneration() @@ -152,16 +138,6 @@ public Feature getLabelFeature() return labelFeature; } - public Feature getSourceFeature() - { - return sourceFeature; - } - - public Feature getTargetFeature() - { - return targetFeature; - } - public Feature getScoreFeature() { return scoreFeature; @@ -186,9 +162,4 @@ public boolean isMultiLabels() { return isMultiLabels; } - - public List getResult() - { - return result; - } } 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 98db34b99ee..78721cf2d0a 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 @@ -39,7 +39,6 @@ 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.cas.TOP; import org.apache.uima.jcas.tcas.Annotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -377,40 +376,52 @@ public Optional getRenderer() featureSupportRegistry)); } - public static void extractSuggestion(ExtractionContext ctx, TOP predictedFS) + public static List extractSuggestions(ExtractionContext ctx) { - var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); - var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), ctx.isMultiLabels()); - var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); - var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); - - var source = (AnnotationFS) predictedFS.getFeatureValue(ctx.getSourceFeature()); - var target = (AnnotationFS) predictedFS.getFeatureValue(ctx.getTargetFeature()); - - var originalSource = findEquivalentSpan(ctx.getOriginalCas(), source); - var originalTarget = findEquivalentSpan(ctx.getOriginalCas(), target); - if (originalSource.isEmpty() || originalTarget.isEmpty()) { - LOG.debug("Unable to find source or target of predicted relation in original CAS"); - return; - } + // TODO Use adapter instead - once the method is no longer static + var sourceFeature = ctx.getPredictedType().getFeatureByBaseName(FEAT_REL_SOURCE); + var targetFeature = ctx.getPredictedType().getFeatureByBaseName(FEAT_REL_TARGET); + + var result = new ArrayList(); + for (var predictedFS : ctx.getPredictionCas().select(ctx.getPredictedType())) { + if (!predictedFS.getBooleanValue(ctx.getPredictionFeature())) { + continue; + } - var position = new RelationPosition(originalSource.get(), originalTarget.get()); - - for (var label : labels) { - var suggestion = RelationSuggestion.builder() // - .withId(RelationSuggestion.NEW_ID) // - .withGeneration(ctx.getGeneration()) // - .withRecommender(ctx.getRecommender()) // - .withDocumentName(ctx.getDocument().getName()) // - .withPosition(position) // - .withLabel(label) // - .withUiLabel(label) // - .withScore(score) // - .withScoreExplanation(scoreExplanation) // - .withAutoAcceptMode(autoAcceptMode) // - .build(); - ctx.getResult().add(suggestion); + var source = (AnnotationFS) predictedFS.getFeatureValue(sourceFeature); + var target = (AnnotationFS) predictedFS.getFeatureValue(targetFeature); + + var originalSource = findEquivalentSpan(ctx.getOriginalCas(), source); + var originalTarget = findEquivalentSpan(ctx.getOriginalCas(), target); + if (originalSource.isEmpty() || originalTarget.isEmpty()) { + LOG.debug("Unable to find source or target of predicted relation in original CAS"); + continue; + } + + var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); + var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), + ctx.isMultiLabels()); + var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); + var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); + var position = new RelationPosition(originalSource.get(), originalTarget.get()); + + for (var label : labels) { + var suggestion = RelationSuggestion.builder() // + .withId(RelationSuggestion.NEW_ID) // + .withGeneration(ctx.getGeneration()) // + .withRecommender(ctx.getRecommender()) // + .withDocumentName(ctx.getDocument().getName()) // + .withPosition(position) // + .withLabel(label) // + .withUiLabel(label) // + .withScore(score) // + .withScoreExplanation(scoreExplanation) // + .withAutoAcceptMode(autoAcceptMode) // + .build(); + result.add(suggestion); + } } + return result; } /** diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java index e4b0914ef31..f974cc3a0a0 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java @@ -40,30 +40,19 @@ public static List extractSuggestions(int aGeneration, CAS var ctx = new ExtractionContext(aGeneration, aRecommender, aDocument, aOriginalCas, aPredictionCas); - for (var predictedFS : aPredictionCas.select(ctx.getPredictedType())) { - if (!predictedFS.getBooleanValue(ctx.getPredictionFeature())) { - continue; - } - - switch (ctx.getLayer().getType()) { - case SpanLayerSupport.TYPE: { - SpanSuggestionSupport.extractSuggestion(ctx, predictedFS); - break; - } - case RelationLayerSupport.TYPE: { - RelationSuggestionSupport.extractSuggestion(ctx, predictedFS); - break; - } - case DocumentMetadataLayerSupport.TYPE: { - MetadataSuggestionSupport.extractSuggestion(ctx, predictedFS); - break; - } - default: - throw new IllegalStateException( - "Unsupported layer type [" + ctx.getLayer().getType() + "]"); - } + switch (ctx.getLayer().getType()) { + case SpanLayerSupport.TYPE: { + return SpanSuggestionSupport.extractSuggestions(ctx); + } + case RelationLayerSupport.TYPE: { + return RelationSuggestionSupport.extractSuggestions(ctx); + } + case DocumentMetadataLayerSupport.TYPE: { + return MetadataSuggestionSupport.extractSuggestions(ctx); + } + default: + throw new IllegalStateException( + "Unsupported layer type [" + ctx.getLayer().getType() + "]"); } - - return ctx.getResult(); } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java index 10869bfeb86..898550b960e 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java @@ -48,7 +48,6 @@ import org.apache.uima.cas.text.AnnotationFS; import org.apache.uima.cas.text.AnnotationPredicates; import org.apache.uima.fit.util.CasUtil; -import org.apache.uima.jcas.cas.TOP; import org.apache.uima.jcas.tcas.Annotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -460,42 +459,47 @@ public Optional getRenderer() featureSupportRegistry, recommenderProperties)); } - public static void extractSuggestion(ExtractionContext ctx, TOP predictedFS) + public static List extractSuggestions(ExtractionContext ctx) { - var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); - var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), ctx.isMultiLabels()); - var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); - var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); - - var predictedAnnotation = (Annotation) predictedFS; - var targetOffsets = getOffsets(ctx.getLayer().getAnchoringMode(), ctx.getOriginalCas(), - predictedAnnotation); - - if (targetOffsets.isEmpty()) { - LOG.debug("Prediction cannot be anchored to [{}]: {}", - ctx.getLayer().getAnchoringMode(), predictedAnnotation); - return; - } + var result = new ArrayList(); + for (var predictedFS : ctx.getPredictionCas(). select(ctx.getPredictedType())) { + if (!predictedFS.getBooleanValue(ctx.getPredictionFeature())) { + continue; + } - var offsets = targetOffsets.get(); - var coveredText = ctx.getDocumentText().substring(offsets.getBegin(), offsets.getEnd()); - - for (var label : labels) { - var suggestion = SpanSuggestion.builder() // - .withId(RelationSuggestion.NEW_ID) // - .withGeneration(ctx.getGeneration()) // - .withRecommender(ctx.getRecommender()) // - .withDocument(ctx.getDocument()) // - .withPosition(offsets) // - .withCoveredText(coveredText) // - .withLabel(label) // - .withUiLabel(label) // - .withScore(score) // - .withScoreExplanation(scoreExplanation) // - .withAutoAcceptMode(autoAcceptMode) // - .build(); - ctx.getResult().add(suggestion); + var anchoringMode = ctx.getLayer().getAnchoringMode(); + var targetOffsets = getOffsets(anchoringMode, ctx.getOriginalCas(), predictedFS); + if (targetOffsets.isEmpty()) { + LOG.debug("Prediction cannot be anchored to [{}]: {}", anchoringMode, predictedFS); + continue; + } + + var autoAcceptMode = getAutoAcceptMode(predictedFS, ctx.getModeFeature()); + var labels = getPredictedLabels(predictedFS, ctx.getLabelFeature(), + ctx.isMultiLabels()); + var score = predictedFS.getDoubleValue(ctx.getScoreFeature()); + var scoreExplanation = predictedFS.getStringValue(ctx.getScoreExplanationFeature()); + var offsets = targetOffsets.get(); + var coveredText = ctx.getDocumentText().substring(offsets.getBegin(), offsets.getEnd()); + + for (var label : labels) { + var suggestion = SpanSuggestion.builder() // + .withId(RelationSuggestion.NEW_ID) // + .withGeneration(ctx.getGeneration()) // + .withRecommender(ctx.getRecommender()) // + .withDocument(ctx.getDocument()) // + .withPosition(offsets) // + .withCoveredText(coveredText) // + .withLabel(label) // + .withUiLabel(label) // + .withScore(score) // + .withScoreExplanation(scoreExplanation) // + .withAutoAcceptMode(autoAcceptMode) // + .build(); + result.add(suggestion); + } } + return result; } /** From 5dbd6fd093d6ebcd4098846cc16104cfa54e0cd0 Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Mon, 25 Dec 2023 16:30:39 +0100 Subject: [PATCH 12/13] #2696 - Document-level recommendations - Change context type for suggestion support from suggestion to recommender --- .../sidebar/MetadataSuggestionSupport.java | 17 +- .../api/RecommendationService.java | 5 +- .../recommendation/api/SuggestionSupport.java | 5 +- .../api/SuggestionSupportRegistry.java | 4 +- .../api/SuggestionSupport_ImplBase.java | 4 +- .../api/model/LearningRecord.java | 17 +- .../recommendation/api/model/Predictions.java | 2 +- .../api/model/SuggestionDocumentGroup.java | 16 +- .../api/model/SuggestionGroup.java | 8 + .../RecommenderServiceAutoConfiguration.java | 2 +- .../relation/RelationSuggestionSupport.java | 17 +- .../render/RecommendationRenderer.java | 28 ++-- ...LayerRecommendtionSupportRegistryImpl.java | 9 +- .../service/RecommendationServiceImpl.java | 77 +++++---- .../span/SpanSuggestionSupport.java | 17 +- .../recommendation/service/Fixtures.java | 4 +- ...ommendationServiceImplIntegrationTest.java | 149 +++++++++--------- ...anSuggestionVisibilityCalculationTest.java | 3 +- 18 files changed, 231 insertions(+), 153 deletions(-) diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java index 70c2aad4103..a390e4662a8 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java @@ -51,16 +51,18 @@ 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.MetadataSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.ExtractionContext; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerAdapter; +import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerTraits; public class MetadataSuggestionSupport - extends SuggestionSupport_ImplBase + extends SuggestionSupport_ImplBase { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -77,9 +79,18 @@ public MetadataSuggestionSupport(RecommendationService aRecommendationService, } @Override - public boolean accepts(AnnotationSuggestion aContext) + public boolean accepts(Recommender aContext) { - return aContext instanceof MetadataSuggestion; + if (!DocumentMetadataLayerSupport.TYPE.equals(aContext.getLayer().getType())) { + return false; + } + + var feature = aContext.getFeature(); + if (CAS.TYPE_NAME_STRING.equals(feature.getType()) || feature.isVirtualFeature()) { + return true; + } + + return false; } @Override diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java index 8c8d4c25f55..dcd73f818c1 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/RecommendationService.java @@ -39,6 +39,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.model.RecommenderGeneralSettings; 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; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommenderContext; @@ -304,7 +305,9 @@ Predictions computePredictions(User aSessionOwner, Project aProject, * @param aLayer * the layer to which the suggestions belong. * @param aRecommendations - * the suggestions. + * the suggestions which must all be of the same type, e.g. all + * {@link SpanSuggestion}s. Use e.g. + * {@link SuggestionDocumentGroup#groupsOfType(Class, List)} to generate them. * @param aWindowBegin * the range of the document for which to update the suggestions. * @param aWindowEnd 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 d5e79d31d31..395a4ace3ce 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 @@ -30,13 +30,14 @@ 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.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; import de.tudarmstadt.ukp.inception.support.extensionpoint.Extension; -public interface SuggestionSupport - extends Extension +public interface SuggestionSupport + extends Extension { 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/SuggestionSupportRegistry.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupportRegistry.java index ecb85797fca..42f752a36f9 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupportRegistry.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupportRegistry.java @@ -17,11 +17,11 @@ */ package de.tudarmstadt.ukp.inception.recommendation.api; -import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.support.extensionpoint.ContextLookupExtensionPoint; public interface SuggestionSupportRegistry - extends ContextLookupExtensionPoint> + extends ContextLookupExtensionPoint { } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport_ImplBase.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport_ImplBase.java index a352059588c..68436fc97c4 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport_ImplBase.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/SuggestionSupport_ImplBase.java @@ -41,8 +41,8 @@ import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; import de.tudarmstadt.ukp.inception.support.uima.ICasUtil; -public abstract class SuggestionSupport_ImplBase - implements SuggestionSupport, BeanNameAware +public abstract class SuggestionSupport_ImplBase + implements SuggestionSupport, BeanNameAware { protected final RecommendationService recommendationService; protected final LearningRecordService learningRecordService; diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LearningRecord.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LearningRecord.java index e943076015e..975d28d30e6 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LearningRecord.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/LearningRecord.java @@ -249,11 +249,16 @@ public void setSuggestionType(String aSuggestionType) suggestionType = aSuggestionType; } - public void setTokenText(String tokenText) + public void setTokenText(String aTokenText) { + if (aTokenText == null) { + tokenText = null; + return; + } + // Truncate the token text if it is too long - int targetLength = Math.min(tokenText.length(), TOKEN_TEXT_LENGTH); - this.tokenText = tokenText.substring(0, targetLength); + int targetLength = Math.min(aTokenText.length(), TOKEN_TEXT_LENGTH); + tokenText = aTokenText.substring(0, targetLength); } /** @@ -268,6 +273,12 @@ public String getAnnotation() public void setAnnotation(String aAnnotation) { + if (aAnnotation == null) { + annotation = null; + return; + } + + // Truncate the label if it is too long int targetLength = Math.min(aAnnotation.length(), LABEL_LENGTH); annotation = aAnnotation.substring(0, targetLength); } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java index 11488d772de..1a9b08f3a32 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/Predictions.java @@ -157,7 +157,7 @@ public SuggestionDocumentGroup getGroupedPre Class type, String aDocumentName, AnnotationLayer aLayer, int aWindowBegin, int aWindowEnd) { - return new SuggestionDocumentGroup<>( + return SuggestionDocumentGroup.groupsOfType(type, getFlattenedPredictions(type, aDocumentName, aLayer, aWindowBegin, aWindowEnd)); } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java index 91ae9110dde..e85afc0daaf 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionDocumentGroup.java @@ -43,14 +43,13 @@ public class SuggestionDocumentGroup private Collection> groups; private String documentName; - public SuggestionDocumentGroup() + /** + * Use {@ink #groupByType(List)} or {@link #groupsOfType(Class, List)} instead to ensure that + * never empty groups are created. + */ + private SuggestionDocumentGroup(List aSuggestions) { groups = new ArrayList<>(); - } - - public SuggestionDocumentGroup(List aSuggestions) - { - this(); addAll(SuggestionGroup.group(aSuggestions)); } @@ -65,7 +64,7 @@ public SuggestionDocumentGroup(List aSuggestions) */ @SuppressWarnings("unchecked") public static SuggestionDocumentGroup groupsOfType( - Class type, List aSuggestions) + Class type, List aSuggestions) { var filteredSuggestions = aSuggestions.stream().filter(type::isInstance) // .toList(); @@ -75,7 +74,8 @@ public static SuggestionDocumentGroup groups /** * @param aSuggestions * the list to retrieve suggestions from - * @return a SuggestionDocumentGroup where only suggestions of type V are added + * @return suggestions grouped by suggestion type. There will not be any empty groups in the + * result. */ public static Map, SuggestionDocumentGroup> // groupByType(List aSuggestions) diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionGroup.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionGroup.java index 0bb7cdcea3f..8390025f9ac 100644 --- a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionGroup.java +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/model/SuggestionGroup.java @@ -400,6 +400,14 @@ public static SuggestionGroupCollector colle return new SuggestionGroupCollector(); } + /** + * @param + * type that all of the suggestions must have + * @param aSuggestions + * the suggestions + * @return suggestions grouped by {@code [layer, feature, position]}. There will be no empty + * groups. + */ public static Collection> group( Collection aSuggestions) { 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 e5054270683..983f3a8b2b7 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 @@ -230,7 +230,7 @@ public RelationSuggestionSupport relationRecommendationSupport( @Bean public SuggestionSupportRegistry layerRecommendtionSupportRegistry( - @Lazy @Autowired(required = false) List> aExtensions) + @Lazy @Autowired(required = false) List aExtensions) { return new LayerRecommendtionSupportRegistryImpl(aExtensions); } 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 78721cf2d0a..04a4a7d9fb8 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 @@ -49,6 +49,7 @@ 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; @@ -58,6 +59,7 @@ 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; @@ -69,7 +71,7 @@ import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; public class RelationSuggestionSupport - extends SuggestionSupport_ImplBase + extends SuggestionSupport_ImplBase { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -88,9 +90,18 @@ public RelationSuggestionSupport(RecommendationService aRecommendationService, } @Override - public boolean accepts(AnnotationSuggestion aContext) + public boolean accepts(Recommender aContext) { - return aContext instanceof RelationSuggestion; + if (!RelationLayerSupport.TYPE.equals(aContext.getLayer().getType())) { + return false; + } + + var feature = aContext.getFeature(); + if (CAS.TYPE_NAME_STRING.equals(feature.getType()) || feature.isVirtualFeature()) { + return true; + } + + return false; } /** diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java index f8756a5ddab..662cd589a2c 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/render/RecommendationRenderer.java @@ -19,13 +19,20 @@ import static de.tudarmstadt.ukp.clarin.webanno.model.Mode.ANNOTATION; import static de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionDocumentGroup.groupByType; +import static java.util.function.Function.identity; import static java.util.stream.Collectors.groupingBy; +import java.util.HashMap; +import java.util.Optional; +import java.util.stream.Collectors; + import org.springframework.core.annotation.Order; import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; +import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport; import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupportRegistry; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.config.RecommenderServiceAutoConfiguration; import de.tudarmstadt.ukp.inception.rendering.editorstate.AnnotatorState; import de.tudarmstadt.ukp.inception.rendering.pipeline.RenderStep; @@ -102,6 +109,10 @@ public void render(VDocument aVDoc, RenderRequest aRequest) var suggestionsByLayer = suggestions.stream() .collect(groupingBy(AnnotationSuggestion::getLayerId)); + var recommenderCache = recommendationService.listRecommenders(aRequest.getProject()) + .stream().collect(Collectors.toMap(Recommender::getId, identity())); + var suggestionSupportCache = new HashMap>(); + for (var layer : aRequest.getVisibleLayers()) { if (!layer.isEnabled() || layer.isReadonly()) { continue; @@ -113,23 +124,20 @@ public void render(VDocument aVDoc, RenderRequest aRequest) } for (var suggestionGroup : suggestionsByType.entrySet()) { - var maybeSuggestion = suggestionGroup.getValue().stream() - .filter(group -> !group.isEmpty()) // - .flatMap(group -> group.stream()) // - .findAny(); + var suggestion = suggestionGroup.getValue().iterator().next().iterator().next(); - if (!maybeSuggestion.isPresent()) { + var recommender = recommenderCache.get(suggestion.getRecommenderId()); + if (recommender == null) { continue; } - var maybeSupport = suggestionSupportRegistry - .findGenericExtension(maybeSuggestion.get()); - - if (!maybeSupport.isPresent()) { + var suggestionSupport = suggestionSupportCache.computeIfAbsent(recommender, + suggestionSupportRegistry::findGenericExtension); + if (suggestionSupport.isEmpty()) { continue; } - maybeSupport.get().getRenderer().ifPresent(renderer -> renderer.render(aVDoc, + suggestionSupport.get().getRenderer().ifPresent(renderer -> renderer.render(aVDoc, aRequest, suggestionGroup.getValue(), layer)); } } diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/LayerRecommendtionSupportRegistryImpl.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/LayerRecommendtionSupportRegistryImpl.java index 2c9ccd035d8..57742ff5de1 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/LayerRecommendtionSupportRegistryImpl.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/LayerRecommendtionSupportRegistryImpl.java @@ -25,24 +25,23 @@ import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport; import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupportRegistry; -import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.support.extensionpoint.ExtensionPoint_ImplBase; public class LayerRecommendtionSupportRegistryImpl - extends ExtensionPoint_ImplBase> + extends ExtensionPoint_ImplBase implements SuggestionSupportRegistry { @Autowired public LayerRecommendtionSupportRegistryImpl( - @Lazy @Autowired(required = false) List> aExtensions) + @Lazy @Autowired(required = false) List aExtensions) { super(aExtensions); } @SuppressWarnings("unchecked") @Override - public > Optional findGenericExtension( - AnnotationSuggestion aKey) + public Optional findGenericExtension(Recommender aKey) { return getExtensions().stream() // .filter(e -> e.accepts(aKey)) // diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java index 49e73ab531e..64715be6e7f 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java @@ -31,6 +31,7 @@ import static de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction.extractSuggestions; import static de.tudarmstadt.ukp.inception.rendering.model.Range.rangeCoveringDocument; import static java.util.Collections.emptyList; +import static java.util.function.Function.identity; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; @@ -40,6 +41,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -51,6 +53,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.persistence.NoResultException; @@ -96,7 +99,6 @@ import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPage; import de.tudarmstadt.ukp.inception.annotation.events.AnnotationEvent; import de.tudarmstadt.ukp.inception.annotation.events.DocumentOpenedEvent; -import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; import de.tudarmstadt.ukp.inception.annotation.storage.CasStorageSession; import de.tudarmstadt.ukp.inception.documents.api.DocumentService; import de.tudarmstadt.ukp.inception.documents.event.AfterCasWrittenEvent; @@ -111,6 +113,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService; import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderFactoryRegistry; import de.tudarmstadt.ukp.inception.recommendation.api.RecommenderTypeSystemUtils; +import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupport; import de.tudarmstadt.ukp.inception.recommendation.api.SuggestionSupportRegistry; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.AutoAcceptMode; @@ -179,7 +182,7 @@ public class RecommendationServiceImpl private final ProjectService projectService; private final ApplicationEventPublisher applicationEventPublisher; private final PreferencesService preferencesService; - private final SuggestionSupportRegistry layerRecommendtionSupportRegistry; + private final SuggestionSupportRegistry suggestionSupportRegistry; private final ConcurrentMap trainingTaskCounter; private final ConcurrentMap states; @@ -221,7 +224,7 @@ public RecommendationServiceImpl(PreferencesService aPreferencesService, projectService = aProjectService; entityManager = aEntityManager; applicationEventPublisher = aApplicationEventPublisher; - layerRecommendtionSupportRegistry = aLayerRecommendtionSupportRegistry; + suggestionSupportRegistry = aLayerRecommendtionSupportRegistry; trainingTaskCounter = new ConcurrentHashMap<>(); states = new ConcurrentHashMap<>(); @@ -603,6 +606,9 @@ private void autoAccept(AjaxRequestTarget aTarget, User aUser, SourceDocument aD } var page = (AnnotationPage) aTarget.getPage(); + if (!page.isEditable()) { + return; + } var predictions = getPredictions(aUser, aDocument.getProject()); if (predictions == null || predictions.isEmpty()) { @@ -610,10 +616,6 @@ private void autoAccept(AjaxRequestTarget aTarget, User aUser, SourceDocument aD return; } - if (!page.isEditable()) { - return; - } - CAS cas; try { cas = page.getEditorCas(); @@ -623,7 +625,10 @@ private void autoAccept(AjaxRequestTarget aTarget, User aUser, SourceDocument aD return; } - var count = 0; + var accepted = 0; + var recommenderCache = listRecommenders(aDocument.getProject()).stream() + .collect(Collectors.toMap(Recommender::getId, identity())); + var suggestionSupportCache = new HashMap>(); for (var prediction : predictions.getPredictionsByDocument(aDocument.getName())) { if (prediction.getAutoAcceptMode() != aAutoAcceptMode) { continue; @@ -636,30 +641,35 @@ private void autoAccept(AjaxRequestTarget aTarget, User aUser, SourceDocument aD prediction.clearAutoAccept(); } - var layer = schemaService.getLayer(prediction.getLayerId()); - var feature = schemaService.getFeature(prediction.getFeature(), layer); - var adapter = schemaService.getAdapter(layer); + var recommender = recommenderCache.get(prediction.getRecommenderId()); + if (recommender == null) { + continue; + } + + var suggestionSupport = suggestionSupportCache.computeIfAbsent(recommender, + suggestionSupportRegistry::findGenericExtension); + if (suggestionSupport.isEmpty()) { + continue; + } + + var feature = recommender.getFeature(); + var adapter = schemaService.getAdapter(recommender.getLayer()); adapter.silenceEvents(); try { - var rls = layerRecommendtionSupportRegistry.findGenericExtension(prediction); - if (rls.isPresent()) { - rls.get().acceptSuggestion(null, aDocument, aUser.getUsername(), cas, adapter, - feature, prediction, AUTO_ACCEPT, ACCEPTED); - rls.get().acceptSuggestion(null, aDocument, aUser.getUsername(), cas, - (SpanAdapter) adapter, feature, prediction, AUTO_ACCEPT, ACCEPTED); - count++; - } + suggestionSupport.get().acceptSuggestion(null, aDocument, aUser.getUsername(), cas, + adapter, feature, prediction, AUTO_ACCEPT, ACCEPTED); + accepted++; } catch (AnnotationException e) { LOG.debug("Not auto-accepting suggestion: {}", e.getMessage()); } } - predictions.log(LogMessage.info(this, "Auto-accepted [%d] suggestions", count)); - LOG.debug("Auto-accepted [{}] suggestions", count); + predictions.log(LogMessage.info(this, "Auto-accepted [%d] suggestions", accepted)); + LOG.debug("Auto-accepted [{}] suggestions", accepted); - if (count > 0) { + if (accepted > 0) { try { page.writeEditorCas(cas); } @@ -1043,8 +1053,8 @@ public AnnotationFS correctSuggestion(String aSessionOwner, SourceDocument aDocu var layer = schemaService.getLayer(aOriginalSuggestion.getLayerId()); var feature = schemaService.getFeature(aOriginalSuggestion.getFeature(), layer); - var originalRls = layerRecommendtionSupportRegistry - .findGenericExtension(aOriginalSuggestion); + var originalRls = suggestionSupportRegistry + .findGenericExtension(getRecommender(aOriginalSuggestion)); if (originalRls.isPresent()) { // If the action was a correction (i.e. suggestion label != annotation value) then // generate a rejection for the original value - we do not want the original value to @@ -1053,8 +1063,8 @@ public AnnotationFS correctSuggestion(String aSessionOwner, SourceDocument aDocu aLocation); } - var correctedRls = layerRecommendtionSupportRegistry - .findGenericExtension(aCorrectedSuggestion); + var correctedRls = suggestionSupportRegistry + .findGenericExtension(getRecommender(aCorrectedSuggestion)); if (correctedRls.isPresent()) { var adapter = schemaService.getAdapter(layer); @@ -1075,8 +1085,9 @@ public AnnotationBaseFS acceptSuggestion(String aSessionOwner, SourceDocument aD var layer = schemaService.getLayer(aSuggestion.getLayerId()); var feature = schemaService.getFeature(aSuggestion.getFeature(), layer); var adapter = schemaService.getAdapter(layer); + var recommender = getRecommender(aSuggestion); - var rls = layerRecommendtionSupportRegistry.findGenericExtension(aSuggestion); + var rls = suggestionSupportRegistry.findGenericExtension(recommender); if (rls.isPresent()) { return rls.get().acceptSuggestion(aSessionOwner, aDocument, aDataOwner, aCas, adapter, @@ -1851,7 +1862,11 @@ public void calculateSuggestionVisibility(Strin return; } - var rls = layerRecommendtionSupportRegistry.findGenericExtension(maybeSuggestion.get()); + // All suggestions in the group must be of the same type. Even if they come from different + // recommenders, the recommenders must all be producing the same type of suggestion, so we + // can happily just take one of them in order to locate the suggestion support. + var recommender = getRecommender(maybeSuggestion.get()); + var rls = suggestionSupportRegistry.findGenericExtension(recommender); if (rls.isPresent()) { rls.get().calculateSuggestionVisibility(aSessionOwner, aDocument, aCas, aDataOwner, @@ -1997,7 +2012,7 @@ public void rejectSuggestion(String aSessionOwner, SourceDocument aDocument, Str AnnotationSuggestion suggestion, LearningRecordChangeLocation aAction) throws AnnotationException { - var rls = layerRecommendtionSupportRegistry.findGenericExtension(suggestion); + var rls = suggestionSupportRegistry.findGenericExtension(getRecommender(suggestion)); if (rls.isPresent()) { rls.get().rejectSuggestion(aSessionOwner, aDocument, aDataOwner, suggestion, aAction); @@ -2010,7 +2025,7 @@ public void skipSuggestion(String aSessionOwner, SourceDocument aDocument, Strin AnnotationSuggestion suggestion, LearningRecordChangeLocation aAction) throws AnnotationException { - var rls = layerRecommendtionSupportRegistry.findGenericExtension(suggestion); + var rls = suggestionSupportRegistry.findGenericExtension(getRecommender(suggestion)); if (rls.isPresent()) { rls.get().skipSuggestion(aSessionOwner, aDocument, aDataOwner, suggestion, aAction); @@ -2025,7 +2040,7 @@ public void logRecord(String aSessionOwner, SourceDocument aDocument, String aDa { LearningRecord record = null; - var rls = layerRecommendtionSupportRegistry.findGenericExtension(aSuggestion); + var rls = suggestionSupportRegistry.findGenericExtension(getRecommender(aSuggestion)); if (rls.isPresent()) { record = rls.get().toLearningRecord(aDocument, aDataOwner, aSuggestion, aFeature, diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java index 898550b960e..106ba941da1 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java @@ -62,6 +62,7 @@ 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.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; @@ -72,6 +73,7 @@ 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.Offset; +import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; 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.SuggestionGroup; @@ -84,7 +86,7 @@ import de.tudarmstadt.ukp.inception.schema.api.feature.FeatureSupportRegistry; public class SpanSuggestionSupport - extends SuggestionSupport_ImplBase + extends SuggestionSupport_ImplBase { private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -106,9 +108,18 @@ public SpanSuggestionSupport(RecommendationService aRecommendationService, } @Override - public boolean accepts(AnnotationSuggestion aContext) + public boolean accepts(Recommender aContext) { - return aContext instanceof SpanSuggestion; + if (!SpanLayerSupport.TYPE.equals(aContext.getLayer().getType())) { + return false; + } + + var feature = aContext.getFeature(); + if (CAS.TYPE_NAME_STRING.equals(feature.getType()) || feature.isVirtualFeature()) { + return true; + } + + return false; } @Override 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 c1be4b712cd..4bc0266f1a4 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 @@ -72,7 +72,7 @@ public static SuggestionDocumentGroup makeSpanSuggestionGroup( suggestions.add(suggestion); } - return new SuggestionDocumentGroup<>(suggestions); + return SuggestionDocumentGroup.groupsOfType(SpanSuggestion.class, suggestions); } public static SuggestionDocumentGroup makeRelationSuggestionGroup( @@ -90,6 +90,6 @@ public static SuggestionDocumentGroup makeRelationSuggestion suggestions.add(suggestion); } - return new SuggestionDocumentGroup<>(suggestions); + return SuggestionDocumentGroup.groupsOfType(RelationSuggestion.class, suggestions); } } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java index 8cdba07b2d5..8821b35fd9d 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplIntegrationTest.java @@ -64,6 +64,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; import de.tudarmstadt.ukp.dkpro.core.api.ner.type.NamedEntity; import de.tudarmstadt.ukp.inception.annotation.feature.string.StringFeatureSupport; +import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanAdapter; import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.annotation.storage.CasStorageSession; @@ -107,9 +108,9 @@ public class RecommendationServiceImplIntegrationTest private FeatureSupportRegistryImpl featureSupportRegistry; private LayerRecommendtionSupportRegistryImpl layerRecommendtionSupportRegistry; private Project project; - private AnnotationLayer layer; - private Recommender rec; - private AnnotationFeature feature; + private AnnotationLayer spanLayer; + private Recommender spanLayerRecommender; + private AnnotationFeature spanLayerFeature; @BeforeEach public void setUp() throws Exception @@ -127,12 +128,11 @@ public void setUp() throws Exception featureSupportRegistry.init(); project = createProject(PROJECT_NAME); - layer = createAnnotationLayer(); - layer.setProject(project); - feature = createAnnotationFeature(layer, "value"); + spanLayer = createSpanLayer(NamedEntity._TypeName); + spanLayerFeature = createAnnotationFeature(spanLayer, "value"); - rec = buildRecommender(project, feature); - sut.createOrUpdateRecommender(rec); + spanLayerRecommender = buildRecommender(spanLayerFeature); + sut.createOrUpdateRecommender(spanLayerRecommender); } @AfterEach @@ -149,13 +149,13 @@ public void thatApplicationContextStarts() @Test public void listRecommenders_WithOneEnabledRecommender_ShouldReturnStoredRecommender() { - sut.createOrUpdateRecommender(rec); + sut.createOrUpdateRecommender(spanLayerRecommender); - var enabledRecommenders = sut.listEnabledRecommenders(rec.getLayer()); + var enabledRecommenders = sut.listEnabledRecommenders(spanLayerRecommender.getLayer()); assertThat(enabledRecommenders) // .as("Check that the previously created recommender is found") // - .containsExactly(rec); + .containsExactly(spanLayerRecommender); } @SuppressWarnings("unchecked") @@ -168,7 +168,7 @@ public void getNumOfEnabledRecommenders_WithOneEnabledRecommender() assertThat(recommenderFactoryRegistry.getFactory("nummy")).isNotNull(); - sut.createOrUpdateRecommender(rec); + sut.createOrUpdateRecommender(spanLayerRecommender); assertThat(sut.countEnabledRecommenders()).isEqualTo(1); } @@ -176,8 +176,8 @@ public void getNumOfEnabledRecommenders_WithOneEnabledRecommender() @Test public void getNumOfEnabledRecommenders_WithNoEnabledRecommender() { - rec.setEnabled(false); - testEntityManager.persist(rec); + spanLayerRecommender.setEnabled(false); + testEntityManager.persist(spanLayerRecommender); assertThat(sut.countEnabledRecommenders()).isEqualTo(0); } @@ -185,18 +185,18 @@ public void getNumOfEnabledRecommenders_WithNoEnabledRecommender() @Test public void getRecommenders_WithOneEnabledRecommender_ShouldReturnStoredRecommender() { - assertThat(sut.getEnabledRecommender(rec.getId())) + assertThat(sut.getEnabledRecommender(spanLayerRecommender.getId())) .as("Check that only the previously created recommender is found").isPresent() - .contains(rec); + .contains(spanLayerRecommender); } @Test public void getRecommenders_WithOnlyDisabledRecommender_ShouldReturnEmptyList() { - rec.setEnabled(false); - testEntityManager.persist(rec); + spanLayerRecommender.setEnabled(false); + testEntityManager.persist(spanLayerRecommender); - assertThat(sut.getEnabledRecommender(rec.getId())) // + assertThat(sut.getEnabledRecommender(spanLayerRecommender.getId())) // .as("Check that no recommender is found") // .isEmpty(); } @@ -219,13 +219,13 @@ public void monkeyPatchTypeSystem_WithNer_CreatesScoreFeatures() throws Exceptio when(schemaService.getFullProjectTypeSystem(project)) .thenReturn(typeSystem2TypeSystemDescription(jCas.getTypeSystem())); - when(schemaService.listAnnotationFeature(project)).thenReturn(asList(feature)); + when(schemaService.listAnnotationFeature(project)).thenReturn(asList(spanLayerFeature)); doCallRealMethod().when(schemaService).upgradeCas(any(CAS.class), any(CAS.class), any(TypeSystemDescription.class)); sut.cloneAndMonkeyPatchCAS(project, jCas.getCas(), jCas.getCas()); - Type type = CasUtil.getType(jCas.getCas(), layer.getName()); + Type type = CasUtil.getType(jCas.getCas(), spanLayer.getName()); assertThat(type.getFeatures()) // .extracting(Feature::getShortName) // @@ -234,9 +234,9 @@ public void monkeyPatchTypeSystem_WithNer_CreatesScoreFeatures() throws Exceptio "begin", // "end", // "value", // - feature.getName() + FEATURE_NAME_SCORE_SUFFIX, // - feature.getName() + FEATURE_NAME_SCORE_EXPLANATION_SUFFIX, // - feature.getName() + FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX, // + spanLayerFeature.getName() + FEATURE_NAME_SCORE_SUFFIX, // + spanLayerFeature.getName() + FEATURE_NAME_SCORE_EXPLANATION_SUFFIX, // + spanLayerFeature.getName() + FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX, // "identifier", // FEATURE_NAME_IS_PREDICTION); } @@ -247,45 +247,40 @@ void testUpsertSpanFeature() throws Exception { var docOwner = "dummy"; var doc = SourceDocument.builder() // + .withId(1l) // .withProject(project) // .build(); - var feature = AnnotationFeature.builder() // - .withName(NamedEntity._FeatName_value) // - .withType(CAS.TYPE_NAME_STRING) // - .build(); - var layer = AnnotationLayer.builder() // - .forJCasClass(NamedEntity.class) // - .build(); - var adapter = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, layer, + var adapter = new SpanAdapter(layerSupportRegistry, featureSupportRegistry, null, spanLayer, () -> asList(), asList()); - when(schemaService.getLayer(anyLong())).thenReturn(layer); + when(schemaService.getLayer(anyLong())).thenReturn(spanLayer); when(schemaService.getAdapter(any())).thenReturn(adapter); - when(schemaService.getFeature(any(), any())).thenReturn(feature); + when(schemaService.getFeature(any(), any())).thenReturn(spanLayerFeature); - layer.setOverlapMode(NO_OVERLAP); + spanLayer.setOverlapMode(NO_OVERLAP); var cas = createJCas(); var targetFS = new NamedEntity(cas, 0, 10); targetFS.addToIndexes(); assertThat(targetFS.getValue()).isNull(); - var s1 = SpanSuggestion.builder().withLabel("V1").withPosition(new Offset(targetFS)) - .build(); + var s1 = SpanSuggestion.builder().withLabel("V1").withRecommender(spanLayerRecommender) + .withPosition(new Offset(targetFS)).build(); sut.acceptSuggestion(USER_NAME, doc, docOwner, cas.getCas(), s1, MAIN_EDITOR); assertThat(targetFS.getValue()) // .as("Label was merged into existing annotation replacing unset label") // .isEqualTo("V1"); - var s2 = SpanSuggestion.builder().withLabel("V2").withPosition(new Offset(targetFS)) - .build(); + var s2 = SpanSuggestion.builder().withLabel("V2").withRecommender(spanLayerRecommender) + .withPosition(new Offset(targetFS)).build(); sut.acceptSuggestion(USER_NAME, doc, docOwner, cas.getCas(), s2, MAIN_EDITOR); assertThat(targetFS.getValue()) // .as("Label was merged into existing annotation replacing previous label") // .isEqualTo("V2"); - var s3 = SpanSuggestion.builder().withLabel("V3").withPosition(new Offset(10, 20)).build(); + var s3 = SpanSuggestion.builder().withLabel("V3").withRecommender(spanLayerRecommender) + .withPosition(new Offset(10, 20)).build(); sut.acceptSuggestion(USER_NAME, doc, docOwner, cas.getCas(), s3, MAIN_EDITOR); assertThat(cas.select(NamedEntity.class).asList()) // @@ -295,22 +290,22 @@ void testUpsertSpanFeature() throws Exception tuple(0, 10, "V2"), // tuple(10, 20, "V3")); - layer.setOverlapMode(ANY_OVERLAP); + spanLayer.setOverlapMode(ANY_OVERLAP); cas.reset(); targetFS = new NamedEntity(cas, 0, 10); targetFS.addToIndexes(); assertThat(targetFS.getValue()).isNull(); - var s4 = SpanSuggestion.builder().withLabel("V1").withPosition(new Offset(targetFS)) - .build(); + var s4 = SpanSuggestion.builder().withLabel("V1").withRecommender(spanLayerRecommender) + .withPosition(new Offset(targetFS)).build(); sut.acceptSuggestion(USER_NAME, doc, docOwner, cas.getCas(), s4, MAIN_EDITOR); assertThat(targetFS.getValue()) // .as("Label was merged into existing annotation replacing unset label") // .isEqualTo("V1"); - var s5 = SpanSuggestion.builder().withLabel("V2").withPosition(new Offset(targetFS)) - .build(); + var s5 = SpanSuggestion.builder().withLabel("V2").withRecommender(spanLayerRecommender) + .withPosition(new Offset(targetFS)).build(); sut.acceptSuggestion(USER_NAME, doc, docOwner, cas.getCas(), s5, MAIN_EDITOR); assertThat(cas.select(NamedEntity.class).asList()) // @@ -320,7 +315,8 @@ void testUpsertSpanFeature() throws Exception tuple(0, 10, "V1"), // tuple(0, 10, "V2")); - var s6 = SpanSuggestion.builder().withLabel("V3").withPosition(new Offset(10, 20)).build(); + var s6 = SpanSuggestion.builder().withLabel("V3").withRecommender(spanLayerRecommender) + .withPosition(new Offset(10, 20)).build(); sut.acceptSuggestion(USER_NAME, doc, docOwner, cas.getCas(), s6, MAIN_EDITOR); assertThat(cas.select(NamedEntity.class).asList()) // @@ -334,7 +330,8 @@ void testUpsertSpanFeature() throws Exception new NamedEntity(cas, 0, 10).addToIndexes(); new NamedEntity(cas, 0, 10).addToIndexes(); - var s7 = SpanSuggestion.builder().withLabel("V4").withPosition(new Offset(0, 10)).build(); + var s7 = SpanSuggestion.builder().withLabel("V4").withRecommender(spanLayerRecommender) + .withPosition(new Offset(0, 10)).build(); sut.acceptSuggestion(USER_NAME, doc, docOwner, cas.getCas(), s7, MAIN_EDITOR); assertThat(cas.select(NamedEntity.class).asList()) // @@ -352,14 +349,10 @@ void testUpsertSpanFeature() throws Exception public void thatSpanSuggestionsCanBeRecorded() { var sourceDoc = createSourceDocument("doc"); - var layer = createAnnotationLayer("layer"); - var feature = createAnnotationFeature(layer, FEATURE_NAME); - var rec = Recommender.builder().withId(1337l).withName("testRecommender").withLayer(layer) - .withFeature(feature).build(); var suggestion = SpanSuggestion.builder() // .withId(42) // - .withRecommender(rec) // + .withRecommender(spanLayerRecommender) // .withDocument(sourceDoc) // .withPosition(7, 14) // .withCoveredText("aCoveredText") // @@ -369,17 +362,18 @@ public void thatSpanSuggestionsCanBeRecorded() .withScoreExplanation("Test confidence") // .build(); - sut.logRecord(USER_NAME, sourceDoc, USER_NAME, suggestion, feature, ACCEPTED, MAIN_EDITOR); + sut.logRecord(USER_NAME, sourceDoc, USER_NAME, suggestion, spanLayerFeature, ACCEPTED, + MAIN_EDITOR); - var records = sut.listLearningRecords(USER_NAME, USER_NAME, layer); + var records = sut.listLearningRecords(USER_NAME, USER_NAME, spanLayer); assertThat(records).hasSize(1); LearningRecord record = records.get(0); assertThat(record).hasFieldOrProperty("id") // .hasFieldOrPropertyWithValue("sourceDocument", sourceDoc) // .hasFieldOrPropertyWithValue("user", USER_NAME) // - .hasFieldOrPropertyWithValue("layer", layer) // - .hasFieldOrPropertyWithValue("annotationFeature", feature) // + .hasFieldOrPropertyWithValue("layer", spanLayer) // + .hasFieldOrPropertyWithValue("annotationFeature", spanLayerFeature) // .hasFieldOrPropertyWithValue("offsetBegin", 7) // .hasFieldOrPropertyWithValue("offsetEnd", 14) // .hasFieldOrPropertyWithValue("offsetBegin2", -1) // @@ -395,10 +389,9 @@ public void thatSpanSuggestionsCanBeRecorded() public void thatRelationSuggestionsCanBeRecorded() { var sourceDoc = createSourceDocument("doc"); - var layer = createAnnotationLayer("layer"); + var layer = createRelationLayer("layer"); var feature = createAnnotationFeature(layer, FEATURE_NAME); - var rec = Recommender.builder().withId(1337l).withName("testRecommender").withLayer(layer) - .withFeature(feature).build(); + var rec = buildRecommender(feature); var suggestion = RelationSuggestion.builder().withId(42).withRecommender(rec) .withDocument(sourceDoc).withPosition(new RelationPosition(7, 14, 21, 28)) @@ -433,15 +426,12 @@ void thatListingRecordsForRendering() { var sourceDoc1 = createSourceDocument("doc1"); var sourceDoc2 = createSourceDocument("doc2"); - var layer1 = createAnnotationLayer("layer1"); - var layer2 = createAnnotationLayer("layer2"); + var layer1 = createSpanLayer("layer1"); + var layer2 = createSpanLayer("layer2"); var feature1 = createAnnotationFeature(layer1, "feat1"); var feature2 = createAnnotationFeature(layer2, "feat1"); - - var rec1 = Recommender.builder().withId(1337l).withName("testRecommender").withLayer(layer1) - .withFeature(feature1).build(); - var rec2 = Recommender.builder().withId(1337l).withName("testRecommender2") - .withLayer(layer2).withFeature(feature2).build(); + var rec1 = buildRecommender(feature1); + var rec2 = buildRecommender(feature2); Offset position = new Offset(7, 14); sut.logRecord(USER_NAME, sourceDoc1, USER_NAME, @@ -484,7 +474,7 @@ private SourceDocument createSourceDocument(String aName) return testEntityManager.persist(doc); } - private AnnotationLayer createAnnotationLayer(String aType) + private AnnotationLayer createSpanLayer(String aType) { var l = new AnnotationLayer(); l.setProject(project); @@ -498,6 +488,20 @@ private AnnotationLayer createAnnotationLayer(String aType) return testEntityManager.persist(l); } + private AnnotationLayer createRelationLayer(String aType) + { + var l = new AnnotationLayer(); + l.setProject(project); + l.setEnabled(true); + l.setName(aType); + l.setReadonly(false); + l.setType(RelationLayerSupport.TYPE); + l.setUiName(aType); + l.setAnchoringMode(false, false); + + return testEntityManager.persist(l); + } + private AnnotationFeature createAnnotationFeature(AnnotationLayer aLayer, String aName) { var f = new AnnotationFeature(); @@ -517,22 +521,17 @@ private Project createProject(String aName) return testEntityManager.persist(l); } - private AnnotationLayer createAnnotationLayer() - { - return createAnnotationLayer(NamedEntity._TypeName); - } - - private Recommender buildRecommender(Project aProject, AnnotationFeature aFeature) + private Recommender buildRecommender(AnnotationFeature aFeature) { var r = new Recommender(); r.setLayer(aFeature.getLayer()); r.setFeature(aFeature); - r.setProject(aProject); + r.setProject(aFeature.getProject()); r.setAlwaysSelected(true); r.setSkipEvaluation(false); r.setMaxRecommendations(3); r.setTool("dummyRecommenderTool"); - return r; + return testEntityManager.persist(r); } } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java index 1d4ca843425..a608d91ee24 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionVisibilityCalculationTest.java @@ -235,7 +235,8 @@ public void thatOverlappingSuggestionsAreNotHiddenWhenStackingIsEnabled() throws .withLabel("blah") // .withPosition(1, 2) // .build(); - var suggestions = new SuggestionDocumentGroup<>(asList(suggestion1, suggestion2)); + var suggestions = SuggestionDocumentGroup.groupsOfType(SpanSuggestion.class, + asList(suggestion1, suggestion2)); sut.calculateSuggestionVisibility(TEST_USER, doc, cas.getCas(), TEST_USER, layer, suggestions, 0, 2); From db28f12cb39e8106dd9ecca3ca81269d38a1091d Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Mon, 25 Dec 2023 21:55:01 +0100 Subject: [PATCH 13/13] #2696 - Document-level recommendations - Complete move of extraction code into suggestion supports --- ...MetadataLayerSupportAutoConfiguration.java | 2 +- .../MetadataSuggestionSupport.java | 5 +- .../MetadataSuggestionExtractionTest.java | 26 +++++- .../recommendation/api/SuggestionSupport.java | 4 + .../relation/RelationSuggestionSupport.java | 3 +- .../service/RecommendationServiceImpl.java | 17 +++- .../service/SuggestionExtraction.java | 58 ------------- .../span/SpanSuggestionSupport.java | 3 +- .../RecommendationServiceImplTest.java | 84 ------------------- .../span/SpanSuggestionExtractionTest.java | 29 ++++++- 10 files changed, 74 insertions(+), 157 deletions(-) rename inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/{sidebar => recommendation}/MetadataSuggestionSupport.java (98%) rename inception/{inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service => inception-layer-docmetadata/src/test/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation}/MetadataSuggestionExtractionTest.java (78%) delete mode 100644 inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/config/DocumentMetadataLayerSupportAutoConfiguration.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/config/DocumentMetadataLayerSupportAutoConfiguration.java index 9f4758831a4..dbbf7eea046 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/config/DocumentMetadataLayerSupportAutoConfiguration.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/config/DocumentMetadataLayerSupportAutoConfiguration.java @@ -33,8 +33,8 @@ import de.tudarmstadt.ukp.inception.ui.core.docanno.event.DocumentMetadataAnnotationActionUndoSupport; import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSingletonCreatingWatcher; import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; +import de.tudarmstadt.ukp.inception.ui.core.docanno.recommendation.MetadataSuggestionSupport; import de.tudarmstadt.ukp.inception.ui.core.docanno.sidebar.DocumentMetadataSidebarFactory; -import de.tudarmstadt.ukp.inception.ui.core.docanno.sidebar.MetadataSuggestionSupport; /** * Provides support for document-level annotations. diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation/MetadataSuggestionSupport.java similarity index 98% rename from inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java rename to inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation/MetadataSuggestionSupport.java index a390e4662a8..84a418097f2 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/sidebar/MetadataSuggestionSupport.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation/MetadataSuggestionSupport.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.ui.core.docanno.sidebar; +package de.tudarmstadt.ukp.inception.ui.core.docanno.recommendation; import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_ALL; import static de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion.FLAG_OVERLAP; @@ -291,7 +291,8 @@ public Optional getRenderer() return Optional.empty(); } - public static List extractSuggestions(ExtractionContext ctx) + @Override + public List extractSuggestions(ExtractionContext ctx) { var result = new ArrayList(); for (var predictedFS : ctx.getPredictionCas().select(ctx.getPredictedType())) { diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/MetadataSuggestionExtractionTest.java b/inception/inception-layer-docmetadata/src/test/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation/MetadataSuggestionExtractionTest.java similarity index 78% rename from inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/MetadataSuggestionExtractionTest.java rename to inception/inception-layer-docmetadata/src/test/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation/MetadataSuggestionExtractionTest.java index be08bd861fe..6480b2a1c45 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/MetadataSuggestionExtractionTest.java +++ b/inception/inception-layer-docmetadata/src/test/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation/MetadataSuggestionExtractionTest.java @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package de.tudarmstadt.ukp.inception.recommendation.service; +package de.tudarmstadt.ukp.inception.ui.core.docanno.recommendation; import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; import static de.tudarmstadt.ukp.inception.support.uima.FeatureStructureBuilder.buildFS; @@ -30,19 +30,34 @@ import org.apache.uima.resource.metadata.TypeSystemDescription; 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.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.MetadataSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.ExtractionContext; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; import de.tudarmstadt.ukp.inception.support.uima.SegmentationUtils; import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; +import de.tudarmstadt.ukp.inception.ui.core.docanno.recommendation.MetadataSuggestionSupport; +@ExtendWith(MockitoExtension.class) class MetadataSuggestionExtractionTest { + private @Mock RecommendationService recommendationService; + private @Mock LearningRecordService learningRecordService; + private @Mock ApplicationEventPublisher applicationEventPublisher; + private @Mock AnnotationSchemaService schemaService; + private Project project; private SourceDocument document; private TypeSystemDescription tsd; @@ -50,6 +65,8 @@ class MetadataSuggestionExtractionTest private TypeDescription metadataType; private FeatureDescription metadataLabelFeature; + private MetadataSuggestionSupport sut; + @BeforeEach void setup() throws Exception { @@ -73,6 +90,9 @@ void setup() throws Exception SegmentationUtils.splitSentences(originalCas); SegmentationUtils.tokenize(originalCas); + + sut = new MetadataSuggestionSupport(recommendationService, learningRecordService, + applicationEventPublisher, schemaService); } @Test @@ -103,8 +123,8 @@ void testDocumentMetadataExtraction() throws Exception .withFeature(FEATURE_NAME_IS_PREDICTION, true) // .buildAndAddToIndexes(); - var suggestions = SuggestionExtraction.extractSuggestions(1, originalCas, predictionCas, - document, recommender); + var ctx = new ExtractionContext(0, recommender, document, originalCas, predictionCas); + var suggestions = sut.extractSuggestions(ctx); assertThat(suggestions) // .filteredOn(a -> a instanceof MetadataSuggestion) // 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 395a4ace3ce..3d83e2ada1a 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 @@ -18,6 +18,7 @@ package de.tudarmstadt.ukp.inception.recommendation.api; import java.util.Collection; +import java.util.List; import java.util.Optional; import org.apache.uima.cas.AnnotationBaseFS; @@ -32,6 +33,7 @@ import de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.model.SuggestionGroup; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.ExtractionContext; import de.tudarmstadt.ukp.inception.schema.api.adapter.AnnotationException; import de.tudarmstadt.ukp.inception.schema.api.adapter.TypeAdapter; import de.tudarmstadt.ukp.inception.support.extensionpoint.Extension; @@ -62,4 +64,6 @@ LearningRecord toLearningRecord(SourceDocument aDocument, String aUsername, LearningRecordUserAction aUserAction, LearningRecordChangeLocation aLocation); Optional getRenderer(); + + List extractSuggestions(ExtractionContext aCtx); } 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 04a4a7d9fb8..dde66836c6a 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 @@ -387,7 +387,8 @@ public Optional getRenderer() featureSupportRegistry)); } - public static List extractSuggestions(ExtractionContext ctx) + @Override + public List extractSuggestions(ExtractionContext ctx) { // TODO Use adapter instead - once the method is no longer static var sourceFeature = ctx.getPredictedType().getFeatureByBaseName(FEAT_REL_SOURCE); diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java index 64715be6e7f..77899ba1534 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImpl.java @@ -28,7 +28,6 @@ import static de.tudarmstadt.ukp.inception.recommendation.api.model.LearningRecordUserAction.SKIPPED; import static de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionCapability.PREDICTION_USES_TEXT_ONLY; import static de.tudarmstadt.ukp.inception.recommendation.api.recommender.TrainingCapability.TRAINING_NOT_SUPPORTED; -import static de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction.extractSuggestions; import static de.tudarmstadt.ukp.inception.rendering.model.Range.rangeCoveringDocument; import static java.util.Collections.emptyList; import static java.util.function.Function.identity; @@ -129,6 +128,7 @@ 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; +import de.tudarmstadt.ukp.inception.recommendation.api.recommender.ExtractionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.PredictionContext; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngine; import de.tudarmstadt.ukp.inception.recommendation.api.recommender.RecommendationEngineFactory; @@ -1736,6 +1736,15 @@ void generateSuggestions(Predictions aIncomingPredictions, PredictionContext aCt var sessionOwner = aIncomingPredictions.getSessionOwner(); var recommender = aEngine.getRecommender(); + // Extract the suggestions from the data which the recommender has written into the CAS + var maybeSupportRegistry = suggestionSupportRegistry.findGenericExtension(recommender); + if (maybeSupportRegistry.isEmpty()) { + LOG.debug("There is no comparible suggestion support for {} - skipping prediction"); + aIncomingPredictions.log(LogMessage.warn(recommender.getName(), // + "Prediction skipped since there is no compatible suggestion support.")); + return; + } + // Perform the actual prediction aIncomingPredictions.log(LogMessage.info(recommender.getName(), "Generating predictions for layer [%s]...", recommender.getLayer().getUiName())); @@ -1745,9 +1754,9 @@ void generateSuggestions(Predictions aIncomingPredictions, PredictionContext aCt var predictedRange = aEngine.predict(aCtx, aPredictionCas, aPredictionBegin, aPredictionEnd); - // Extract the suggestions from the data which the recommender has written into the CAS - var generatedSuggestions = extractSuggestions(aIncomingPredictions.getGeneration(), - aOriginalCas, aPredictionCas, aDocument, recommender); + var extractionContext = new ExtractionContext(aIncomingPredictions.getGeneration(), + recommender, aDocument, aOriginalCas, aPredictionCas); + var generatedSuggestions = maybeSupportRegistry.get().extractSuggestions(extractionContext); // Reconcile new suggestions with suggestions from previous run var reconciliationResult = reconcile(aActivePredictions, aDocument, recommender, diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java deleted file mode 100644 index f974cc3a0a0..00000000000 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/service/SuggestionExtraction.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.service; - -import java.util.List; - -import org.apache.uima.cas.CAS; - -import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; -import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationLayerSupport; -import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanLayerSupport; -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.recommender.ExtractionContext; -import de.tudarmstadt.ukp.inception.recommendation.relation.RelationSuggestionSupport; -import de.tudarmstadt.ukp.inception.recommendation.span.SpanSuggestionSupport; -import de.tudarmstadt.ukp.inception.ui.core.docanno.layer.DocumentMetadataLayerSupport; -import de.tudarmstadt.ukp.inception.ui.core.docanno.sidebar.MetadataSuggestionSupport; - -public class SuggestionExtraction -{ - public static List extractSuggestions(int aGeneration, CAS aOriginalCas, - CAS aPredictionCas, SourceDocument aDocument, Recommender aRecommender) - { - var ctx = new ExtractionContext(aGeneration, aRecommender, aDocument, aOriginalCas, - aPredictionCas); - - switch (ctx.getLayer().getType()) { - case SpanLayerSupport.TYPE: { - return SpanSuggestionSupport.extractSuggestions(ctx); - } - case RelationLayerSupport.TYPE: { - return RelationSuggestionSupport.extractSuggestions(ctx); - } - case DocumentMetadataLayerSupport.TYPE: { - return MetadataSuggestionSupport.extractSuggestions(ctx); - } - default: - throw new IllegalStateException( - "Unsupported layer type [" + ctx.getLayer().getType() + "]"); - } - } -} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java index 106ba941da1..b97b01aac50 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionSupport.java @@ -470,7 +470,8 @@ public Optional getRenderer() featureSupportRegistry, recommenderProperties)); } - public static List extractSuggestions(ExtractionContext ctx) + @Override + public List extractSuggestions(ExtractionContext ctx) { var result = new ArrayList(); for (var predictedFS : ctx.getPredictionCas(). select(ctx.getPredictedType())) { diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java index e8132f4ec91..140dc2bf272 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/service/RecommendationServiceImplTest.java @@ -17,29 +17,12 @@ */ package de.tudarmstadt.ukp.inception.recommendation.service; -import static de.tudarmstadt.ukp.clarin.webanno.model.AnchoringMode.TOKENS; -import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX; -import static de.tudarmstadt.ukp.inception.recommendation.api.RecommendationService.FEATURE_NAME_IS_PREDICTION; -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 de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction.extractSuggestions; -import static de.tudarmstadt.ukp.inception.support.uima.FeatureStructureBuilder.buildFS; -import static java.util.Arrays.asList; -import static org.apache.uima.cas.CAS.TYPE_NAME_ANNOTATION; -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; -import static org.apache.uima.fit.factory.CasFactory.createCas; -import static org.apache.uima.fit.factory.TypeSystemDescriptionFactory.createTypeSystemDescription; -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 java.util.Arrays; -import org.apache.uima.UIMAFramework; import org.apache.uima.fit.testing.factory.TokenBuilder; -import org.apache.uima.jcas.tcas.Annotation; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,7 +33,6 @@ import de.tudarmstadt.ukp.clarin.webanno.security.model.User; 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.span.SpanLayerSupport; import de.tudarmstadt.ukp.inception.recommendation.api.model.AnnotationSuggestion; import de.tudarmstadt.ukp.inception.recommendation.api.model.Offset; import de.tudarmstadt.ukp.inception.recommendation.api.model.Predictions; @@ -129,70 +111,4 @@ void testReconciliation() throws Exception AnnotationSuggestion::getAge) // .containsExactlyInAnyOrder(tuple(0, "aged", 1), tuple(3, "added", 0)); } - - @Test - void testExtractSuggestionsWithSpanSuggestions() throws Exception - { - var tsd = UIMAFramework.getResourceSpecifierFactory().createTypeSystemDescription(); - var predType = tsd.addType("Prediction", null, TYPE_NAME_ANNOTATION); - predType.addFeature("value", null, TYPE_NAME_STRING); - predType.addFeature("value" + FEATURE_NAME_SCORE_SUFFIX, null, TYPE_NAME_DOUBLE); - predType.addFeature("value" + FEATURE_NAME_SCORE_EXPLANATION_SUFFIX, null, - TYPE_NAME_STRING); - predType.addFeature("value" + FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX, null, TYPE_NAME_STRING); - predType.addFeature(FEATURE_NAME_IS_PREDICTION, null, TYPE_NAME_BOOLEAN); - - var targetCas = createCas(mergeTypeSystems(asList(tsd, createTypeSystemDescription()))); - tokenBuilder.buildTokens(targetCas.getJCas(), "This is a test .\nAnother sentence here ."); - - var layer = AnnotationLayer.builder() // - .withId(1l) // - .forUimaType(targetCas.getTypeSystem().getType(predType.getName())) // - .withType(SpanLayerSupport.TYPE) // - .withAnchoringMode(TOKENS) // - .build(); - var feature = AnnotationFeature.builder() // - .withId(1l) // - .withName("value") // - .withLayer(layer1) // - .build(); - var recommender = Recommender.builder() // - .withId(1l) // - .withEnabled(true) // - .withLayer(layer) // - .withFeature(feature) // - .withMaxRecommendations(3) // - .build(); - - var suggestionCas = createCas(mergeTypeSystems(asList(tsd, createTypeSystemDescription()))); - buildFS(suggestionCas, predType.getName()) // - .withFeature(Annotation._FeatName_begin, 0) // - .withFeature(Annotation._FeatName_end, 4) // - .withFeature("value", "foo") // - .withFeature("value" + FEATURE_NAME_SCORE_SUFFIX, 1.0d) // - .withFeature("value" + FEATURE_NAME_SCORE_EXPLANATION_SUFFIX, "one") // - .withFeature(FEATURE_NAME_IS_PREDICTION, true) // - .buildAndAddToIndexes(); - buildFS(suggestionCas, predType.getName()) // - .withFeature(Annotation._FeatName_begin, 5) // - .withFeature(Annotation._FeatName_end, 12) // - .withFeature("value", "bar") // - .withFeature("value" + FEATURE_NAME_SCORE_SUFFIX, 0.5d) // - .withFeature("value" + FEATURE_NAME_SCORE_EXPLANATION_SUFFIX, "two") // - .withFeature(FEATURE_NAME_IS_PREDICTION, true) // - .buildAndAddToIndexes(); - - var suggestions = extractSuggestions(0, targetCas, suggestionCas, doc1, recommender); - - assertThat(suggestions) // - .extracting( // - AnnotationSuggestion::getLabel, // - AnnotationSuggestion::getScore, // - s -> s.getScoreExplanation().orElse(null), // - AnnotationSuggestion::getPosition) - .containsExactly( // - tuple("foo", 1.0d, "one", new Offset(0, 4)), // - tuple("bar", 0.5d, "two", new Offset(5, 9))); - } - } diff --git a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java index 33ef1f5c1e5..cacd3d87f8c 100644 --- a/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java +++ b/inception/inception-recommendation/src/test/java/de/tudarmstadt/ukp/inception/recommendation/span/SpanSuggestionExtractionTest.java @@ -36,6 +36,10 @@ import org.apache.uima.jcas.tcas.Annotation; 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; @@ -45,20 +49,35 @@ 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.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.Offset; import de.tudarmstadt.ukp.inception.recommendation.api.model.Recommender; import de.tudarmstadt.ukp.inception.recommendation.api.model.SpanSuggestion; -import de.tudarmstadt.ukp.inception.recommendation.service.SuggestionExtraction; +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.FeatureSupportRegistry; import de.tudarmstadt.ukp.inception.support.uima.SegmentationUtils; +@ExtendWith(MockitoExtension.class) class SpanSuggestionExtractionTest { + private @Mock RecommendationService recommendationService; + private @Mock LearningRecordService learningRecordService; + private @Mock ApplicationEventPublisher applicationEventPublisher; + private @Mock AnnotationSchemaService schemaService; + private @Mock FeatureSupportRegistry featureSupportRegistry; + private @Mock RecommenderProperties recommenderProperties; + private TokenBuilder tokenBuilder; private Project project; private SourceDocument document; private CAS originalCas; + private SpanSuggestionSupport sut; + @BeforeEach void setup() throws Exception { @@ -78,6 +97,10 @@ void setup() throws Exception SegmentationUtils.splitSentences(originalCas); SegmentationUtils.tokenize(originalCas); + + sut = new SpanSuggestionSupport(recommendationService, learningRecordService, + applicationEventPublisher, schemaService, featureSupportRegistry, + recommenderProperties); } @Test @@ -109,8 +132,8 @@ void testSpanExtraction() throws Exception .withFeature(FEATURE_NAME_IS_PREDICTION, true) // .buildAndAddToIndexes(); - var suggestions = SuggestionExtraction.extractSuggestions(1, originalCas, predictionCas, - document, recommender); + var ctx = new ExtractionContext(0, recommender, document, originalCas, predictionCas); + var suggestions = sut.extractSuggestions(ctx); assertThat(suggestions) // .filteredOn(a -> a instanceof SpanSuggestion) //