Skip to content

Commit

Permalink
#3868 - Allow a recommender to force-create annotations
Browse files Browse the repository at this point in the history
- Extended recommender API to allow force-accepting suggestions into a document when the document is opened for the first time
- Documented the extension
  • Loading branch information
reckart committed Mar 26, 2023
1 parent e49ec81 commit baaf679
Show file tree
Hide file tree
Showing 28 changed files with 366 additions and 139 deletions.
Expand Up @@ -284,7 +284,7 @@ public void acceptSpanSuggestion(User aUser, AnnotationLayer aLayer, SpanSuggest
// Request clearing selection and when onFeatureValueUpdated is triggered as a callback
// from the update event created by upsertSpanFeature.
AnnotationFeature feature = schemaService.getFeature(suggestion.getFeature(), layer);
recommendationService.upsertSpanFeature(schemaService, sourceDoc, username, cas, layer,
recommendationService.upsertSpanFeature(sourceDoc, username, cas, layer,
feature, value, suggestion.getBegin(), suggestion.getEnd());

// Save CAS after annotation has been created
Expand Down
Expand Up @@ -69,7 +69,6 @@
import de.tudarmstadt.ukp.clarin.webanno.api.CasProvider;
import de.tudarmstadt.ukp.clarin.webanno.api.DocumentService;
import de.tudarmstadt.ukp.clarin.webanno.api.annotation.keybindings.KeyBindingsPanel;
import de.tudarmstadt.ukp.clarin.webanno.api.event.AfterDocumentResetEvent;
import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationFeature;
import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationLayer;
import de.tudarmstadt.ukp.clarin.webanno.model.Project;
Expand All @@ -96,6 +95,7 @@
import de.tudarmstadt.ukp.inception.active.learning.event.ActiveLearningSessionStartedEvent;
import de.tudarmstadt.ukp.inception.active.learning.event.ActiveLearningSuggestionOfferedEvent;
import de.tudarmstadt.ukp.inception.active.learning.strategy.UncertaintySamplingStrategy;
import de.tudarmstadt.ukp.inception.annotation.events.DocumentOpenedEvent;
import de.tudarmstadt.ukp.inception.annotation.events.FeatureValueUpdatedEvent;
import de.tudarmstadt.ukp.inception.annotation.layer.relation.RelationCreatedEvent;
import de.tudarmstadt.ukp.inception.annotation.layer.span.SpanCreatedEvent;
Expand Down Expand Up @@ -1353,7 +1353,7 @@ private void refreshAvailableSuggestions()
}

@OnEvent
public void onDocumentReset(AfterDocumentResetEvent aEvent)
public void onDocumentOpenedEvent(DocumentOpenedEvent aEvent)
{
// If active learning is not active, update the sidebar in case the session auto-terminated
ActiveLearningUserState alState = alStateModel.getObject();
Expand Down
Expand Up @@ -1062,6 +1062,10 @@ private void realWriteCas(SourceDocument aDocument, String aUserName, CAS aCas)
{
analyze(aDocument.getProject(), aDocument.getName(), aDocument.getId(), aUserName, aCas);

log.debug("Writing annotation document [{}] ({}) for user [{}] in project [{}] ({})",
aDocument.getName(), aDocument.getId(), aUserName, aDocument.getProject().getName(),
aDocument.getProject().getId());

driver.writeCas(aDocument, aUserName, aCas);
}
}
Expand Up @@ -20,6 +20,7 @@
import org.apache.uima.cas.CAS;
import org.springframework.context.ApplicationEvent;

import de.tudarmstadt.ukp.clarin.webanno.model.AnnotationDocumentState;
import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument;
import de.tudarmstadt.ukp.clarin.webanno.support.wicket.event.HybridApplicationUIEvent;

Expand All @@ -35,15 +36,17 @@ public class DocumentOpenedEvent
private final String annotator;
// user who opened the document
private final String opener;
private final AnnotationDocumentState stateBeforeOpening;

public DocumentOpenedEvent(Object aSource, CAS aCas, SourceDocument aDocument,
String aAnnotator, String aOpener)
AnnotationDocumentState aStateBeforeOpening, String aAnnotator, String aOpener)
{
super(aSource);
cas = aCas;
document = aDocument;
annotator = aAnnotator;
opener = aOpener;
stateBeforeOpening = aStateBeforeOpening;
}

public CAS getCas()
Expand All @@ -65,4 +68,9 @@ public String getAnnotator()
{
return annotator;
}

public AnnotationDocumentState getStateBeforeOpening()
{
return stateBeforeOpening;
}
}
Expand Up @@ -171,7 +171,7 @@ public void requestRender(AjaxRequestTarget aTarget)

if (getModelObject().getDocument() != null) {
// Fire render event into UI
extensionRegistry.fireRenderRequested(getModelObject());
extensionRegistry.fireRenderRequested(aTarget, getModelObject());
send(getPage(), Broadcast.BREADTH, new RenderRequestedEvent(aTarget));
}
}
Expand Down
Expand Up @@ -69,7 +69,7 @@ default void handleAction(AnnotationActionHandler aActionHandler, AnnotatorState
// Do nothing by default
}

default void renderRequested(AnnotatorState aState)
default void renderRequested(AjaxRequestTarget aTarget, AnnotatorState aState)
{
// Do nothing by default
}
Expand Down
Expand Up @@ -40,7 +40,7 @@ void fireAction(AnnotationActionHandler aActionHandler, AnnotatorState aModelObj
AjaxRequestTarget aTarget, CAS aCas, VID aParamId, String aAction)
throws IOException, AnnotationException;

void fireRenderRequested(AnnotatorState aState);
void fireRenderRequested(AjaxRequestTarget aTarget, AnnotatorState aState);

void generateContextMenuItems(List<IMenuItem> aItems);
}
Expand Up @@ -122,10 +122,10 @@ public void fireAction(AnnotationActionHandler aActionHandler, AnnotatorState aM
}

@Override
public void fireRenderRequested(AnnotatorState aState)
public void fireRenderRequested(AjaxRequestTarget aTarget, AnnotatorState aState)
{
for (AnnotationEditorExtension ext : getExtensions()) {
ext.renderRequested(aState);
ext.renderRequested(aTarget, aState);
}
}

Expand Down
Expand Up @@ -65,6 +65,7 @@ public interface AnnotatorState

// REC: would be very nice if we didn't need the mode - the behaviors specific to annotation,
// curation, automation, correction, etc. should be local to the respective modules / pages
@Deprecated
Mode getMode();

// ---------------------------------------------------------------------------------------------
Expand Down
Expand Up @@ -15,35 +15,16 @@ When making changes to the API, just copy the stuff from there over to this docu
[[_external_recommender_api_overview]]
== Overview
This section describes the External Recommender API for INCEpTION. An external recommender is a
This section describes the External Recommender API for {product-name}. An external recommender is a
classifier whose functionality is exposed via a HTTP web service. It can predict annotations for
given documents and optionally be trained on new data. This document describes the endpoints a web
service needs to expose so it can be used with INCEpTION. The documents that are exchanged are in
service needs to expose so it can be used with {product-name}. The documents that are exchanged are in
form of a UIMA CAS. For sending, they have to be serialized to CAS XMI. For receiving, it has to be
deserialized back. There are two main libraries available that manage CAS handling, one is the UIMA
Java SDK, the other one dkpro-cassis (Python).
=== Version information
[%hardbreaks]
__Version__ : 1.0.0
=== Contact information
[%hardbreaks]
__Contact Email__ : inception-users@googlegroups.com
=== License information
[%hardbreaks]
__License__ : Apache 2.0
__License URL__ : http://www.apache.org/licenses/LICENSE-2.0.html
__Terms of service__ : https://inception-project.github.io
deserialized back. There are two main libraries available that manage CAS handling, one is the
link:https://uima.apache.org[Apache UIMA Java SDK], the other one link:https://github.com/dkpro/dkpro-cassis#dkpro-cassis[dkpro-cassis] (Python).
[[_external_recommender_api_paths]]
== Paths
== API Endpoints
[[_external_recommender_api_predictcas]]
=== Predict annotations for a single document
Expand Down Expand Up @@ -291,5 +272,16 @@ __required__|Type system XML of the CAS +
**Example** : `"<?xml version=\"1.0\" encoding=\"UTF-8\"?> <typeSystemDescription xmlns=\"http://uima.apache.org/resourceSpecifier\"> <types> <typeDescription> <name>uima.tcas.DocumentAnnotation</name> <description/> <supertypeName>uima.tcas.Annotation</supertypeName> <features> <featureDescription> <name>language</name> <description/> <rangeTypeName>uima.cas.String</rangeTypeName> </featureDescription> </features> </typeDescription> </types> </typeSystemDescription>"`|string
|===
== Encoding annotation suggestions
This section explains how annotation suggestions can be encoded in the response to a `predict` call.
Note that a recommender can only produce suggestions for one feature on one layer. The name of the layer and feature are contained in the request to the `predict` call and only suggestions generated for that specific layer and feature will be processed by {product-name} when the call returns.
For the purpose of producing annotation suggestions, this specific layer is extended with additional features that can be set. Some of these features start with the name of the feature (we use `<FEATURE_NAME>` as a placeholder for the actual feature name below) to be predicted and then add a suffix:
* `inception_internal_predicted`: this boolean feature indicates that an annotation was added by the external recommender. It allows the system to distinguish between annotations that already existed in the document and annotations that the recommender has created. Only annotations where this flag is set to `true` will be processed by {product-name}.
* `<FEATURE_NAME>`: this feature takes the label that the external recommender assigns.
* `<FEATURE_NAME>_score` (optional): this floating-point (double) feature can be used to indicate the score assigned to a predicted label.
* `<FEATURE_NAME>_score_explanation` (optional): this string feature can be used to provide an explanation for the score. This explanation is shown on the annotation page when the user inspects a particular suggestion (note that not all editors may support displaying explanations).
* `<FEATURE_NAME>_auto_accept` (optional): this feature can be set to `on-first-access` to force-accept an annotation into a document when an annotator accesses a document for the first time. This should only be used in conjunction with non-trainable recommenders and with the option **Wait for suggestions from non-trainable recommenders when opening document** in the recommender project settings. Thus, when an annotator opens a document for the first time, the system would wait for recommendations by non-trainable (pre-trained) recommenders and then directly accept any of the suggestions that the recommender has marked to uto-accept on-first-access. When the annotator resets a document via the action bar, this procedure is also followed. This provides a convenient way of "pre-annotating" documents with the help of external recommenders. Note though that an annotator has to actually open a document in order for this process to trigger.
Expand Up @@ -41,7 +41,6 @@
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;
import de.tudarmstadt.ukp.inception.schema.AnnotationSchemaService;
import de.tudarmstadt.ukp.inception.schema.adapter.AnnotationException;

/**
Expand All @@ -56,6 +55,7 @@ public interface RecommendationService
String FEATURE_NAME_IS_PREDICTION = "inception_internal_predicted";
String FEATURE_NAME_SCORE_SUFFIX = "_score";
String FEATURE_NAME_SCORE_EXPLANATION_SUFFIX = "_score_explanation";
String FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX = "_auto_accept";

int MAX_RECOMMENDATIONS_DEFAULT = 3;
int MAX_RECOMMENDATIONS_CAP = 10;
Expand Down Expand Up @@ -150,8 +150,6 @@ void setEvaluatedRecommenders(User aUser, AnnotationLayer layer,
* Uses the given annotation suggestion to create a new annotation or to update a feature in an
* existing annotation.
*
* @param annotationService
* the annotation schema service
* @param aDocument
* the source document to which the annotations belong
* @param aUsername
Expand All @@ -173,14 +171,12 @@ void setEvaluatedRecommenders(User aUser, AnnotationLayer layer,
* @throws AnnotationException
* if there was an annotation-level problem
*/
int upsertSpanFeature(AnnotationSchemaService annotationService, SourceDocument aDocument,
String aUsername, CAS aCas, AnnotationLayer layer, AnnotationFeature aFeature,
String aValue, int aBegin, int aEnd)
int upsertSpanFeature(SourceDocument aDocument, String aUsername, CAS aCas,
AnnotationLayer layer, AnnotationFeature aFeature, String aValue, int aBegin, int aEnd)
throws AnnotationException;

int upsertRelationFeature(AnnotationSchemaService annotationService, SourceDocument aDocument,
String aUsername, CAS aCas, AnnotationLayer layer, AnnotationFeature aFeature,
RelationSuggestion aSuggestion)
int upsertRelationFeature(SourceDocument aDocument, String aUsername, CAS aCas,
AnnotationLayer layer, AnnotationFeature aFeature, RelationSuggestion aSuggestion)
throws AnnotationException;

/**
Expand Down
Expand Up @@ -81,11 +81,13 @@ public abstract class AnnotationSuggestion
protected final String uiLabel;
protected final double score;
protected final String scoreExplanation;

private AutoAcceptMode autoAcceptMode;
private int hidingFlags = 0;

public AnnotationSuggestion(int aId, long aRecommenderId, String aRecommenderName,
long aLayerId, String aFeature, String aDocumentName, String aLabel, String aUiLabel,
double aScore, String aScoreExplanation)
double aScore, String aScoreExplanation, AutoAcceptMode aAutoAcceptMode)
{
label = aLabel;
uiLabel = aUiLabel;
Expand All @@ -97,6 +99,7 @@ public AnnotationSuggestion(int aId, long aRecommenderId, String aRecommenderNam
scoreExplanation = aScoreExplanation;
recommenderId = aRecommenderId;
documentName = aDocumentName;
autoAcceptMode = aAutoAcceptMode;
}

public AnnotationSuggestion(AnnotationSuggestion aObject)
Expand All @@ -111,6 +114,7 @@ public AnnotationSuggestion(AnnotationSuggestion aObject)
scoreExplanation = aObject.scoreExplanation;
recommenderId = aObject.recommenderId;
documentName = aObject.documentName;
autoAcceptMode = aObject.autoAcceptMode;
}

public int getId()
Expand Down Expand Up @@ -209,6 +213,16 @@ public boolean isVisible()
return hidingFlags == 0;
}

public AutoAcceptMode getAutoAcceptMode()
{
return autoAcceptMode;
}

public void clearAutoAccept()
{
autoAcceptMode = AutoAcceptMode.NEVER;
}

public VID getVID()
{
String payload = new VID(layerId, (int) recommenderId, id).toString();
Expand Down
@@ -0,0 +1,23 @@
/*
* Licensed to the Technische Universität Darmstadt under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The Technische Universität Darmstadt
* licenses this file to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.tudarmstadt.ukp.inception.recommendation.api.model;

public enum AutoAcceptMode
{
NEVER, ON_FIRST_ACCESS
}
Expand Up @@ -32,20 +32,21 @@ public class RelationSuggestion

public RelationSuggestion(int aId, Recommender aRecommender, long aLayerId, String aFeature,
String aDocumentName, AnnotationFS aSource, AnnotationFS aTarget, String aLabel,
String aUiLabel, double aScore, String aScoreExplanation)
String aUiLabel, double aScore, String aScoreExplanation,
AutoAcceptMode aAutoAcceptMode)
{
this(aId, aRecommender.getId(), aRecommender.getName(), aLayerId, aFeature, aDocumentName,
aSource.getBegin(), aSource.getEnd(), aTarget.getBegin(), aTarget.getEnd(), aLabel,
aUiLabel, aScore, aScoreExplanation);
aUiLabel, aScore, aScoreExplanation, aAutoAcceptMode);
}

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)
String aScoreExplanation, AutoAcceptMode aAutoAcceptMode)
{
super(aId, aRecommenderId, aRecommenderName, aLayerId, aFeature, aDocumentName, aLabel,
aUiLabel, aScore, aScoreExplanation);
aUiLabel, aScore, aScoreExplanation, aAutoAcceptMode);

position = new RelationPosition(aSourceBegin, aSourceEnd, aTargetBegin, aTargetEnd);
}
Expand Down
Expand Up @@ -32,19 +32,21 @@ public class SpanSuggestion

public SpanSuggestion(int aId, Recommender aRecommender, long aLayerId, String aFeature,
String aDocumentName, Offset aOffset, String aCoveredText, String aLabel,
String aUiLabel, double aScore, String aScoreExplanation)
String aUiLabel, double aScore, String aScoreExplanation,
AutoAcceptMode aAutoAcceptMode)
{
this(aId, aRecommender.getId(), aRecommender.getName(), aLayerId, aFeature, aDocumentName,
aOffset.getBegin(), aOffset.getEnd(), aCoveredText, aLabel, aUiLabel, aScore,
aScoreExplanation);
aScoreExplanation, aAutoAcceptMode);
}

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)
String aLabel, String aUiLabel, double aScore, String aScoreExplanation,
AutoAcceptMode aAutoAcceptMode)
{
super(aId, aRecommenderId, aRecommenderName, aLayerId, aFeature, aDocumentName, aLabel,
aUiLabel, aScore, aScoreExplanation);
aUiLabel, aScore, aScoreExplanation, aAutoAcceptMode);

position = new Offset(aBegin, aEnd);
coveredText = aCoveredText;
Expand Down
Expand Up @@ -17,6 +17,7 @@
*/
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;
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;
Expand Down Expand Up @@ -212,6 +213,12 @@ protected Feature getScoreExplanationFeature(CAS aCas)
return getPredictedType(aCas).getFeatureByBaseName(scoreExplanationFeature);
}

protected Feature getModeFeature(CAS aCas)
{
String scoreExplanationFeature = featureName + FEATURE_NAME_AUTO_ACCEPT_MODE_SUFFIX;
return getPredictedType(aCas).getFeatureByBaseName(scoreExplanationFeature);
}

protected Feature getIsPredictionFeature(CAS aCas)
{
return getPredictedType(aCas).getFeatureByBaseName(FEATURE_NAME_IS_PREDICTION);
Expand Down

0 comments on commit baaf679

Please sign in to comment.