From 5360ce7be53e3ad8726307c2794073693606c91a Mon Sep 17 00:00:00 2001 From: Richard Eckart de Castilho Date: Fri, 28 Feb 2025 07:16:44 +0100 Subject: [PATCH] Issue #5314: Option for the assistant to check annotations - Added check option accessible by right-clicking on a span annotation - Added watch mode that continually monitors new annotations and comments if it thinks something needs to be changed - Added new queuing mode to the scheduling service which just executes tasks one after the other - Added debug mode toggle to the assistant sidebar --- inception/inception-assistant/pom.xml | 24 +- .../inception/assistant/AssistantService.java | 14 ++ .../assistant/AssistantServiceImpl.java | 126 +++++++++- .../ukp/inception/assistant/ChatContext.java | 70 +++++- .../assistant/config/AssistantProperties.java | 2 - .../config/AssistantPropertiesImpl.java | 15 -- .../contextmenu/CheckAnnotationTask.java | 65 +++++- .../assistant/model/MCallResponse.java | 139 +++++++++++ .../assistant/model/MChatMessage.java | 2 +- .../assistant/sidebar/AssistantSidebar.html | 6 +- .../assistant/sidebar/AssistantSidebar.java | 92 +++++++- .../sidebar/AssistantSidebarPrefs.java | 45 ++++ .../sidebar/WatchAnnotationTask.java | 218 ++++++++++++++++++ .../MetadataSuggestionSupport.java | 6 +- .../api/recommender/TrainingInstance.java | 23 ++ .../llm/InteractiveRecommenderSidebar.java | 2 +- .../ukp/inception/scheduling/MatchResult.java | 8 +- .../scheduling/SchedulingServiceImpl.java | 4 + .../project/InviteProjectSettingsPanel.java | 5 +- .../ukp/inception/support/json/JSONUtil.java | 15 +- 20 files changed, 814 insertions(+), 67 deletions(-) create mode 100644 inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/model/MCallResponse.java create mode 100644 inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebarPrefs.java create mode 100644 inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/WatchAnnotationTask.java create mode 100644 inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/TrainingInstance.java diff --git a/inception/inception-assistant/pom.xml b/inception/inception-assistant/pom.xml index 2e40f3721a1..f8af0dffc90 100644 --- a/inception/inception-assistant/pom.xml +++ b/inception/inception-assistant/pom.xml @@ -73,10 +73,6 @@ de.tudarmstadt.ukp.inception.app inception-imls-llm-support - - de.tudarmstadt.ukp.inception.app - inception-scheduling - de.tudarmstadt.ukp.inception.app inception-schema-api @@ -101,6 +97,10 @@ de.tudarmstadt.ukp.inception.app inception-api-editor + + de.tudarmstadt.ukp.inception.app + inception-support-bootstrap + com.knuddels @@ -137,6 +137,14 @@ com.networknt json-schema-validator + + com.github.victools + jsonschema-module-jackson + + + com.github.victools + jsonschema-generator + org.apache.wicket @@ -166,6 +174,14 @@ org.wicketstuff wicketstuff-jquery-ui + + org.wicketstuff + wicketstuff-annotationeventdispatcher + + + org.danekja + jdk-serializable-functional + org.apache.uima diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/AssistantService.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/AssistantService.java index df17c96e1a3..52b24308894 100644 --- a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/AssistantService.java +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/AssistantService.java @@ -21,6 +21,8 @@ import java.util.List; import de.tudarmstadt.ukp.clarin.webanno.model.Project; +import de.tudarmstadt.ukp.inception.assistant.model.MCallResponse; +import de.tudarmstadt.ukp.inception.assistant.model.MMessage; import de.tudarmstadt.ukp.inception.assistant.model.MTextMessage; public interface AssistantService @@ -32,10 +34,22 @@ public interface AssistantService void processUserMessage(String aSessionOwner, Project aProject, MTextMessage aMessage, MTextMessage... aTransientMessage); + void processAgentMessage(String aSessionOwner, Project aProject, MTextMessage aMessage, + MTextMessage... aContextMessages); + MTextMessage processInternalMessageSync(String aSessionOwner, Project aProject, MTextMessage aMessage) throws IOException; + MCallResponse processInternalCallSync(String aSessionOwner, Project aProject, + Class aType, MTextMessage aMessage) + throws IOException; + void clearConversation(String aSessionOwner, Project aProject); + void setDebugMode(String aSessionOwner, Project aProject, boolean aObject); + + boolean isDebugMode(String aSessionOwner, Project aProject); + + void dispatchMessage(String aSessionOwner, Project aProject, MMessage aMessage); } diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/AssistantServiceImpl.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/AssistantServiceImpl.java index d6f4dd391ee..bdd39bb5e80 100644 --- a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/AssistantServiceImpl.java +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/AssistantServiceImpl.java @@ -18,10 +18,12 @@ package de.tudarmstadt.ukp.inception.assistant; import static de.tudarmstadt.ukp.inception.assistant.model.MChatRoles.SYSTEM; +import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.toPrettyJsonString; import static java.lang.Math.floorDiv; import static java.lang.String.join; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; import static org.apache.commons.lang3.ArrayUtils.isNotEmpty; import java.io.IOException; @@ -29,6 +31,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -51,6 +54,7 @@ import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.clarin.webanno.security.model.User; import de.tudarmstadt.ukp.inception.assistant.config.AssistantProperties; +import de.tudarmstadt.ukp.inception.assistant.model.MCallResponse; import de.tudarmstadt.ukp.inception.assistant.model.MChatMessage; import de.tudarmstadt.ukp.inception.assistant.model.MMessage; import de.tudarmstadt.ukp.inception.assistant.model.MRemoveConversationCommand; @@ -147,6 +151,7 @@ public List getAllChatMessages(String aSessionOwner, Project aProj return state.getMessages().stream() // .filter(MTextMessage.class::isInstance) // .map(MTextMessage.class::cast) // + .filter(msg -> state.isDebugMode() || !msg.internal()) // .toList(); } @@ -165,7 +170,7 @@ public List getChatMessages(String aSessionOwner, Project aProject void recordMessage(String aSessionOwner, Project aProject, MChatMessage aMessage) { - if (!properties.isDevMode() && aMessage.ephemeral()) { + if (!isDebugMode(aSessionOwner, aProject) && aMessage.ephemeral()) { return; } @@ -173,10 +178,11 @@ void recordMessage(String aSessionOwner, Project aProject, MChatMessage aMessage state.upsertMessage(aMessage); } - void dispatchMessage(String aSessionOwner, Project aProject, MMessage aMessage) + @Override + public void dispatchMessage(String aSessionOwner, Project aProject, MMessage aMessage) { if (aMessage instanceof MChatMessage chatMessage) { - if (!properties.isDevMode() && chatMessage.internal()) { + if (!isDebugMode(aSessionOwner, aProject) && chatMessage.internal()) { return; } } @@ -190,13 +196,32 @@ void dispatchMessage(String aSessionOwner, Project aProject, MMessage aMessage) public void clearConversation(String aSessionOwner, Project aProject) { synchronized (states) { - states.keySet().removeIf(key -> aSessionOwner.equals(key.user()) - && Objects.equals(aProject.getId(), key.projectId)); + states.entrySet().stream() // + .filter(e -> aSessionOwner.equals(e.getKey().user()) + && Objects.equals(aProject.getId(), e.getKey().projectId)) // + .map(Entry::getValue) // + .forEach(state -> state.clearMessages()); } dispatchMessage(aSessionOwner, aProject, new MRemoveConversationCommand()); } + @Override + public void setDebugMode(String aSessionOwner, Project aProject, boolean aOnOff) + { + synchronized (states) { + getState(aSessionOwner, aProject).setDebugMode(aOnOff); + } + } + + @Override + public boolean isDebugMode(String aSessionOwner, Project aProject) + { + synchronized (states) { + return getState(aSessionOwner, aProject).isDebugMode(); + } + } + @Override public MTextMessage processInternalMessageSync(String aSessionOwner, Project aProject, MTextMessage aMessage) @@ -204,13 +229,76 @@ public MTextMessage processInternalMessageSync(String aSessionOwner, Project aPr { Validate.isTrue(aMessage.internal()); - if (properties.isDevMode()) { + if (isDebugMode(aSessionOwner, aProject)) { recordMessage(aSessionOwner, aProject, aMessage); dispatchMessage(aSessionOwner, aProject, aMessage); } var assistant = new ChatContext(properties, ollamaClient, aSessionOwner, aProject); - return assistant.generate(asList(aMessage)); + return assistant.chat(asList(aMessage)); + } + + @Override + public MCallResponse processInternalCallSync(String aSessionOwner, Project aProject, + Class aType, MTextMessage aMessage) + throws IOException + { + Validate.isTrue(aMessage.internal()); + + if (isDebugMode(aSessionOwner, aProject)) { + recordMessage(aSessionOwner, aProject, aMessage); + dispatchMessage(aSessionOwner, aProject, aMessage); + } + + var assistant = new ChatContext(properties, ollamaClient, aSessionOwner, aProject); + var result = assistant.call(aType, asList(aMessage)); + + if (isDebugMode(aSessionOwner, aProject)) { + var resultMessage = MTextMessage.builder() // + .withRole(SYSTEM).internal().ephemeral() // + .withActor(aMessage.actor()) // + .withMessage("```json\n" + toPrettyJsonString(result.payload()) + "\n```") // + .withPerformance(result.performance()) // + .build(); + recordMessage(aSessionOwner, aProject, resultMessage); + dispatchMessage(aSessionOwner, aProject, resultMessage); + } + + return result; + } + + @Override + public void processAgentMessage(String aSessionOwner, Project aProject, MTextMessage aMessage, + MTextMessage... aContextMessages) + { + var assistant = new ChatContext(properties, ollamaClient, aSessionOwner, aProject); + + // Dispatch message early so the front-end can enter waiting state + dispatchMessage(aSessionOwner, aProject, aMessage); + + try { + var systemMessages = generateSystemMessages(); + + recordMessage(aSessionOwner, aProject, aMessage); + + var recentConversation = limitConversationToContextLength(systemMessages, emptyList(), + emptyList(), aMessage, properties.getChat().getContextLength()); + + var responseMessage = assistant.chat(recentConversation, + (id, r) -> handleStreamedMessageFragment(aSessionOwner, aProject, id, r)); + + recordMessage(aSessionOwner, aProject, responseMessage); + + dispatchMessage(aSessionOwner, aProject, responseMessage.withoutContent()); + } + catch (IOException e) { + var errorMessage = MTextMessage.builder() // + .withActor("Error").withRole(SYSTEM).internal().ephemeral() // + .withMessage("Error: " + e.getMessage()) // + .build(); + recordMessage(aSessionOwner, aProject, errorMessage); + dispatchMessage(aSessionOwner, aProject, errorMessage); + } } @Override @@ -238,7 +326,7 @@ public void processUserMessage(String aSessionOwner, Project aProject, MTextMess // We record the message only now to ensure it is not included in the listMessages above recordMessage(aSessionOwner, aProject, aMessage); - if (properties.isDevMode()) { + if (isDebugMode(aSessionOwner, aProject)) { for (var msg : ephemeralMessages) { recordMessage(aSessionOwner, aProject, msg); dispatchMessage(aSessionOwner, aProject, msg); @@ -249,7 +337,7 @@ public void processUserMessage(String aSessionOwner, Project aProject, MTextMess ephemeralMessages, conversationMessages, aMessage, properties.getChat().getContextLength()); - var responseMessage = assistant.generate(recentConversation, + var responseMessage = assistant.chat(recentConversation, (id, r) -> handleStreamedMessageFragment(aSessionOwner, aProject, id, r)); recordMessage(aSessionOwner, aProject, responseMessage); @@ -448,10 +536,28 @@ private void clearState(String aSessionOwner) private static class AssistentState { private LinkedList messages = new LinkedList<>(); + private boolean debugMode; public List getMessages() { - return new ArrayList<>(messages); + return unmodifiableList(new ArrayList<>(messages)); + } + + public void clearMessages() + { + synchronized (messages) { + messages.clear(); + } + } + + public void setDebugMode(boolean aOnOff) + { + debugMode = aOnOff; + } + + public boolean isDebugMode() + { + return debugMode; } public void upsertMessage(MMessage aMessage) diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/ChatContext.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/ChatContext.java index 6de48e62e14..ecadc49bba8 100644 --- a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/ChatContext.java +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/ChatContext.java @@ -17,6 +17,8 @@ */ package de.tudarmstadt.ukp.inception.assistant; +import static com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON; +import static com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12; import static de.tudarmstadt.ukp.inception.assistant.model.MChatRoles.ASSISTANT; import static java.lang.System.currentTimeMillis; @@ -26,8 +28,15 @@ import java.util.UUID; import java.util.function.BiConsumer; +import com.github.victools.jsonschema.generator.Option; +import com.github.victools.jsonschema.generator.SchemaGenerator; +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; +import com.github.victools.jsonschema.module.jackson.JacksonModule; +import com.github.victools.jsonschema.module.jackson.JacksonOption; + import de.tudarmstadt.ukp.clarin.webanno.model.Project; import de.tudarmstadt.ukp.inception.assistant.config.AssistantProperties; +import de.tudarmstadt.ukp.inception.assistant.model.MCallResponse; import de.tudarmstadt.ukp.inception.assistant.model.MPerformanceMetrics; import de.tudarmstadt.ukp.inception.assistant.model.MReference; import de.tudarmstadt.ukp.inception.assistant.model.MTextMessage; @@ -37,6 +46,7 @@ import de.tudarmstadt.ukp.inception.recommendation.imls.llm.ollama.client.OllamaChatResponse; import de.tudarmstadt.ukp.inception.recommendation.imls.llm.ollama.client.OllamaClient; import de.tudarmstadt.ukp.inception.recommendation.imls.llm.ollama.client.OllamaOptions; +import de.tudarmstadt.ukp.inception.support.json.JSONUtil; public class ChatContext { @@ -44,6 +54,7 @@ public class ChatContext private final OllamaClient ollamaClient; private final String sessionOwner; private final Project project; + private SchemaGenerator generator; public ChatContext(AssistantProperties aProperties, OllamaClient aOllamaClient, String aSessionOwner, Project aProject) @@ -52,6 +63,10 @@ public ChatContext(AssistantProperties aProperties, OllamaClient aOllamaClient, ollamaClient = aOllamaClient; sessionOwner = aSessionOwner; project = aProject; + generator = new SchemaGenerator(new SchemaGeneratorConfigBuilder(DRAFT_2020_12, PLAIN_JSON) // + .with(Option.FORBIDDEN_ADDITIONAL_PROPERTIES_BY_DEFAULT) // + .with(new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED)) // + .build()); } public Project getProject() @@ -64,12 +79,12 @@ public String getSessionOwner() return sessionOwner; } - public MTextMessage generate(List aMessasges) throws IOException + public MTextMessage chat(List aMessasges) throws IOException { - return generate(aMessasges, null); + return chat(aMessasges, null); } - public MTextMessage generate(List aMessasges, + public MTextMessage chat(List aMessasges, BiConsumer aCallback) throws IOException { @@ -121,6 +136,55 @@ public MTextMessage generate(List aMessasges, .build(); } + public MCallResponse call(Class aResult, List aMessasges) + throws IOException + { + var schema = generator.generateSchema(aResult); + + var responseId = UUID.randomUUID(); + var chatProperties = properties.getChat(); + var request = OllamaChatRequest.builder() // + .withModel(chatProperties.getModel()) // + .withStream(true) // + .withMessages(aMessasges.stream() // + .map(msg -> new OllamaChatMessage(msg.role(), msg.message())) // + .toList()) // + .withFormat(schema) // + .withOption(OllamaOptions.NUM_CTX, chatProperties.getContextLength()) // + .withOption(OllamaOptions.TOP_P, chatProperties.getTopP()) // + .withOption(OllamaOptions.TOP_K, chatProperties.getTopK()) // + .withOption(OllamaOptions.REPEAT_PENALTY, chatProperties.getRepeatPenalty()) // + .withOption(OllamaOptions.TEMPERATURE, chatProperties.getTemperature()) // + .build(); + + var references = new LinkedHashMap(); + aMessasges.stream() // + .flatMap(msg -> msg.references().stream()) // + .forEach(r -> references.put(r.id(), r)); + + // Generate the actual response + var startTime = currentTimeMillis(); + var response = ollamaClient.chat(properties.getUrl(), request, null); + var tokens = response.getEvalCount(); + var endTime = currentTimeMillis(); + + var payload = JSONUtil.fromJsonString(aResult, response.getMessage().content()); + + // Send a final and complete message also including final metrics + return MCallResponse.builder(aResult) // + .withId(responseId) // + .withActor(properties.getNickname()) // + .withRole(ASSISTANT) // + .withPayload(payload) // + .withPerformance(MPerformanceMetrics.builder() // + .withDuration(endTime - startTime) // + .withTokens(tokens) // + .build()) // + // Include all refs in the final message again just to be sure + .withReferences(references.values()) // + .build(); + } + private void streamMessage(BiConsumer aCallback, UUID responseId, OllamaChatResponse msg) { diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/config/AssistantProperties.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/config/AssistantProperties.java index ff8a1cbbfdd..e6d8a39bf6a 100644 --- a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/config/AssistantProperties.java +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/config/AssistantProperties.java @@ -25,8 +25,6 @@ public interface AssistantProperties AssistantEmbeddingProperties getEmbedding(); - boolean isDevMode(); - String getNickname(); AssitantUserGuideProperties getUserGuide(); diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/config/AssistantPropertiesImpl.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/config/AssistantPropertiesImpl.java index 72d6b42b5f5..325eb6515a0 100644 --- a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/config/AssistantPropertiesImpl.java +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/config/AssistantPropertiesImpl.java @@ -18,7 +18,6 @@ package de.tudarmstadt.ukp.inception.assistant.config; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("assistant") @@ -40,9 +39,6 @@ public AssistantPropertiesImpl(AssistantDocumentIndexProperties aDocumentIndex) documentIndex = aDocumentIndex; } - @Value("${inception.dev:false}") // Inject system property or use default if not provided - private boolean devMode; - @Override public String getNickname() { @@ -54,17 +50,6 @@ public void setNickname(String aNickname) nickname = aNickname; } - @Override - public boolean isDevMode() - { - return devMode; - } - - public void setDevMode(boolean aDevMode) - { - devMode = aDevMode; - } - @Override public String getUrl() { diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/contextmenu/CheckAnnotationTask.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/contextmenu/CheckAnnotationTask.java index f58cf7cd894..09aef58742b 100644 --- a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/contextmenu/CheckAnnotationTask.java +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/contextmenu/CheckAnnotationTask.java @@ -21,14 +21,19 @@ import static de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasUpgradeMode.AUTO_CAS_UPGRADE; import static de.tudarmstadt.ukp.inception.assistant.model.MChatRoles.SYSTEM; import static de.tudarmstadt.ukp.inception.assistant.model.MChatRoles.USER; +import static de.tudarmstadt.ukp.inception.scheduling.MatchResult.NO_MATCH; +import static de.tudarmstadt.ukp.inception.scheduling.MatchResult.QUEUE_THIS; import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.toPrettyJsonString; import static de.tudarmstadt.ukp.inception.support.uima.ICasUtil.selectAnnotationByAddr; import static java.lang.String.join; import static java.util.Objects.requireNonNull; import static org.apache.commons.lang3.StringUtils.normalizeSpace; +import static org.apache.uima.cas.CAS.TYPE_NAME_BOOLEAN; import java.io.IOException; import java.util.LinkedHashMap; +import java.util.Objects; +import java.util.UUID; import org.apache.commons.lang3.Validate; import org.apache.uima.jcas.tcas.Annotation; @@ -39,15 +44,21 @@ import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; import de.tudarmstadt.ukp.inception.assistant.AssistantService; import de.tudarmstadt.ukp.inception.assistant.model.MTextMessage; +import de.tudarmstadt.ukp.inception.assistant.sidebar.WatchAnnotationTask; import de.tudarmstadt.ukp.inception.documents.api.DocumentService; import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; +import de.tudarmstadt.ukp.inception.scheduling.MatchResult; +import de.tudarmstadt.ukp.inception.scheduling.MatchableTask; import de.tudarmstadt.ukp.inception.scheduling.Task; import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; public class CheckAnnotationTask extends Task + implements MatchableTask { + public static final String TYPE = "CheckAnnotationTask"; + private static final String ACTOR = "Annotation checker"; private @Autowired AnnotationSchemaService schemaService; private @Autowired AssistantService assistantService; @@ -68,12 +79,26 @@ public CheckAnnotationTask(Builder> aBuilder) annotation = aBuilder.annotation; } + @Override + public MatchResult matches(Task aTask) + { + if (aTask instanceof WatchAnnotationTask) { + if (Objects.equals(getProject().getId(), aTask.getProject().getId()) + && Objects.equals(getUser().get(), aTask.getUser().orElse(null))) { + return QUEUE_THIS; + } + } + + return NO_MATCH; + } + @Override public void execute() throws Exception { try (var session = CasStorageSession.open()) { var cas = documentService.readAnnotationCas(document, dataOwner, AUTO_CAS_UPGRADE, SHARED_READ_ONLY_ACCESS); + var ann = selectAnnotationByAddr(cas, annotation.getId()); if (ann == null) { return; @@ -84,10 +109,19 @@ public void execute() throws Exception return; } + var inquiryMsgId = UUID.randomUUID(); + var sessionOwner = getUser().get().getUsername(); + assistantService.dispatchMessage(sessionOwner, getProject(), MTextMessage.builder() // + .withId(inquiryMsgId) // + .withActor(getUser().get().getUiName()) // + .withRole(USER) // + .notDone() // + .build()); + var instance = annotationToJson(ann, contextSentence); - var rewriteTask = MTextMessage.builder() // - .withActor("Annotation checker") // + var rewriteQuestionTask = MTextMessage.builder() // + .withActor(ACTOR) // .withRole(USER).internal().ephemeral() // .withMessage(join("\n", // "Rewrite into a question about whether the annotation is correct with respect to the " @@ -99,11 +133,11 @@ public void execute() throws Exception "```")) // .build(); - var question = assistantService.processInternalMessageSync( - getUser().get().getUsername(), getProject(), rewriteTask); + var rewrittenQuestion = assistantService.processInternalMessageSync(sessionOwner, + getProject(), rewriteQuestionTask); - var contextMessage = MTextMessage.builder() // - .withActor("Annotation checker") // + var inquiryContext = MTextMessage.builder() // + .withActor(ACTOR) // .withRole(SYSTEM).internal().ephemeral() // .withMessage(join("\n", // "The user will ask whether the following annotation is correct.", // @@ -117,14 +151,15 @@ public void execute() throws Exception "```")) // .build(); - var correctionTask = MTextMessage.builder() // + var inquiryTask = MTextMessage.builder() // + .withId(inquiryMsgId) // .withActor(getUser().get().getUiName()) // .withRole(USER) // - .withMessage(question.message()) // + .withMessage(rewrittenQuestion.message()) // .build(); - assistantService.processUserMessage(getUser().get().getUsername(), getProject(), - correctionTask, contextMessage); + assistantService.processUserMessage(sessionOwner, getProject(), inquiryTask, + inquiryContext); } } @@ -143,10 +178,16 @@ private String annotationToJson(Annotation aAnnotation, Annotation aContext) thr instance.put("context", normalizeSpace(context)); var adapter = schemaService.findAdapter(getProject(), aAnnotation); - var attributes = new LinkedHashMap(); + var attributes = new LinkedHashMap(); for (var feature : adapter.listFeatures()) { + if (TYPE_NAME_BOOLEAN.equals(feature.getType())) { + attributes.put(normalizeSpace(feature.getUiName()), + adapter.getFeatureValue(feature, aAnnotation)); + continue; + } + attributes.put(normalizeSpace(feature.getUiName()), - normalizeSpace(adapter.getFeatureValue(feature, aAnnotation))); + normalizeSpace(adapter.renderFeatureValue(aAnnotation, feature.getName()))); } instance.put("annotation", attributes); diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/model/MCallResponse.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/model/MCallResponse.java new file mode 100644 index 00000000000..a3d3c847311 --- /dev/null +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/model/MCallResponse.java @@ -0,0 +1,139 @@ +/* + * 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.assistant.model; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record MCallResponse(UUID id, String role, String actor, boolean internal, + boolean ephemeral, MPerformanceMetrics performance, List references, T payload) + implements MChatMessage +{ + + static final String TYPE_TEXT_MESSAGE = "callResponse"; + + private MCallResponse(Builder aBuilder) + { + this(aBuilder.id, aBuilder.role, aBuilder.actor, aBuilder.internal, aBuilder.ephemeral, + aBuilder.performance, aBuilder.references.values().stream().toList(), + aBuilder.payload); + } + + @JsonProperty(MMessage.TYPE_FIELD) + public String getType() + { + return TYPE_TEXT_MESSAGE; + } + + public static Builder builder(Class aType) + { + return new Builder<>(); + } + + public static final class Builder + { + private UUID id; + private String actor; + private String role; + private boolean internal = false; + private boolean ephemeral = false; + private MPerformanceMetrics performance; + private Map references = new LinkedHashMap<>(); + private T payload; + + private Builder() + { + } + + public Builder withId(UUID aId) + { + id = aId; + return this; + } + + public Builder internal() + { + internal = true; + return this; + } + + public Builder ephemeral() + { + ephemeral = true; + return this; + } + + public Builder withRole(String aRole) + { + role = aRole; + return this; + } + + public Builder withActor(String aActor) + { + actor = aActor; + return this; + } + + public Builder withPayload(T aPayload) + { + payload = aPayload; + return this; + } + + public Builder withPerformance(MPerformanceMetrics aPerformance) + { + performance = aPerformance; + return this; + } + + public Builder withReferences(MReference... aReferences) + { + if (aReferences != null) { + for (var ref : aReferences) { + references.put(ref.id(), ref); + } + } + return this; + } + + public Builder withReferences(Iterable aReferences) + { + if (aReferences != null) { + for (var ref : aReferences) { + references.put(ref.id(), ref); + } + } + return this; + } + + public MCallResponse build() + { + if (id == null) { + id = UUID.randomUUID(); + } + + return new MCallResponse<>(this); + } + } + +} diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/model/MChatMessage.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/model/MChatMessage.java index 3dca467e631..655fc6283b7 100644 --- a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/model/MChatMessage.java +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/model/MChatMessage.java @@ -19,7 +19,7 @@ public sealed interface MChatMessage extends MMessage - permits MTextMessage + permits MTextMessage, MCallResponse { /** * @return the role of the message author diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebar.html b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebar.html index 3e948e020b5..007ea61c42d 100644 --- a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebar.html +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebar.html @@ -22,14 +22,16 @@
-
+
+ + -
+
diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebar.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebar.java index 846ebe1db58..927e0b13123 100644 --- a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebar.java +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebar.java @@ -17,17 +17,34 @@ */ package de.tudarmstadt.ukp.inception.assistant.sidebar; +import static de.tudarmstadt.ukp.inception.assistant.sidebar.AssistantSidebarPrefs.KEY_ASSISTANT_SIDEBAR_PREFS; +import static de.tudarmstadt.ukp.inception.support.lambda.HtmlElementEvents.CHANGE_EVENT; + import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.model.CompoundPropertyModel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; import org.apache.wicket.spring.injection.annot.SpringBean; +import org.wicketstuff.event.annotation.OnEvent; +import de.agilecoders.wicket.extensions.markup.html.bootstrap.icon.FontAwesome6IconType; import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasProvider; import de.tudarmstadt.ukp.clarin.webanno.security.UserDao; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.AnnotationPageBase2; import de.tudarmstadt.ukp.clarin.webanno.ui.annotation.sidebar.AnnotationSidebar_ImplBase; +import de.tudarmstadt.ukp.inception.annotation.events.FeatureValueUpdatedEvent; import de.tudarmstadt.ukp.inception.assistant.AssistantService; import de.tudarmstadt.ukp.inception.assistant.documents.DocumentQueryService; +import de.tudarmstadt.ukp.inception.bootstrap.IconToggleBox; import de.tudarmstadt.ukp.inception.editor.action.AnnotationActionHandler; +import de.tudarmstadt.ukp.inception.preferences.PreferencesService; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; +import de.tudarmstadt.ukp.inception.scheduling.SchedulingService; +import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxFormComponentUpdatingBehavior; +import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxFormSubmittingBehavior; import de.tudarmstadt.ukp.inception.support.lambda.LambdaAjaxLink; +import de.tudarmstadt.ukp.inception.support.lambda.LambdaModelAdapter; public class AssistantSidebar extends AnnotationSidebar_ImplBase @@ -37,31 +54,98 @@ public class AssistantSidebar private @SpringBean UserDao userService; private @SpringBean AssistantService assistantService; private @SpringBean DocumentQueryService documentQueryService; + private @SpringBean SchedulingService schedulingService; + private @SpringBean PreferencesService preferencesService; private AssistantPanel chat; + private CompoundPropertyModel sidebarPrefs; + private IModel debugMode; + public AssistantSidebar(String aId, AnnotationActionHandler aActionHandler, CasProvider aCasProvider, AnnotationPageBase2 aAnnotationPage) { super(aId, aActionHandler, aCasProvider, aAnnotationPage); + sidebarPrefs = new CompoundPropertyModel<>(Model.of(loadSidebarPrefs())); + + debugMode = new LambdaModelAdapter<>( // + () -> assistantService.isDebugMode(userService.getCurrentUsername(), + getModelObject().getProject()), // + onOff -> assistantService.setDebugMode(userService.getCurrentUsername(), + getModelObject().getProject(), onOff)); + chat = new AssistantPanel("chat"); queue(chat); - queue(new LambdaAjaxLink("reindex", this::actionReindex)); + var form = new Form<>("form", sidebarPrefs); + add(form); + + form.add(new LambdaAjaxLink("reindex", this::actionReindex)); + + form.add(new LambdaAjaxLink("clear", this::actionClear)); + + form.add(new IconToggleBox("watchMode") // + .setCheckedIcon(FontAwesome6IconType.eye_s) // + .setCheckedTitle(Model.of("Watching annotation actions and commenting")) // + .setUncheckedIcon(FontAwesome6IconType.eye_slash_s) // + .setUncheckedTitle(Model.of("Not watching annotation actions")) // + .add(new LambdaAjaxFormSubmittingBehavior(CHANGE_EVENT, + _target -> saveSidebarPrefs()))); + + form.add(new IconToggleBox("debugMode") // + .setCheckedIcon(FontAwesome6IconType.bug_s) // + .setCheckedTitle(Model.of("Recording and showing internal messages")) // + .setUncheckedIcon(FontAwesome6IconType.bug_slash_s) // + .setUncheckedTitle(Model.of("Not recoording and showing internal messages")) // + .setModel(debugMode) // + .add(new LambdaAjaxFormComponentUpdatingBehavior(CHANGE_EVENT, + _target -> _target.add(chat)))); + } + + private AssistantSidebarPrefs loadSidebarPrefs() + { + var sessionOwner = userService.getCurrentUser(); + return preferencesService.loadTraitsForUserAndProject(KEY_ASSISTANT_SIDEBAR_PREFS, + sessionOwner, getModelObject().getProject()); + } - queue(new LambdaAjaxLink("clear", this::actionClear)); + private void saveSidebarPrefs() + { + var sessionOwner = userService.getCurrentUser(); + preferencesService.saveTraitsForUserAndProject(KEY_ASSISTANT_SIDEBAR_PREFS, sessionOwner, + getModelObject().getProject(), sidebarPrefs.getObject()); } private void actionReindex(AjaxRequestTarget aTarget) { - documentQueryService.rebuildIndexAsync(getAnnotationPage().getProject()); + documentQueryService.rebuildIndexAsync(getModelObject().getProject()); } private void actionClear(AjaxRequestTarget aTarget) { var sessionOwner = userService.getCurrentUsername(); - var project = getAnnotationPage().getProject(); + var project = getModelObject().getProject(); assistantService.clearConversation(sessionOwner, project); } + + @OnEvent + public void onFeatureValueUpdated(FeatureValueUpdatedEvent aEvent) + { + if (!sidebarPrefs.map(AssistantSidebarPrefs::isWatchMode).orElse(false).getObject() + && aEvent.getNewValue() == null) { + return; + } + + var sessionOwner = userService.getCurrentUser(); + + schedulingService.enqueue(WatchAnnotationTask.builder() // + .withTrigger("Assistant watching") // + .withSessionOwner(sessionOwner) // + .withProject(aEvent.getProject()) // + .withDocument(aEvent.getDocument()) // + .withDataOwner(aEvent.getDocumentOwner()) // + .withAnnotation(VID.of(aEvent.getFS())) // + .build()); + } } diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebarPrefs.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebarPrefs.java new file mode 100644 index 00000000000..e66b63f13fc --- /dev/null +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/AssistantSidebarPrefs.java @@ -0,0 +1,45 @@ +/* + * 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.assistant.sidebar; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import de.tudarmstadt.ukp.inception.preferences.PreferenceKey; +import de.tudarmstadt.ukp.inception.preferences.PreferenceValue; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class AssistantSidebarPrefs + implements PreferenceValue +{ + private static final long serialVersionUID = -5879519353175794875L; + + public static final PreferenceKey KEY_ASSISTANT_SIDEBAR_PREFS = // + new PreferenceKey<>(AssistantSidebarPrefs.class, "annotation/editor/assistant-sidebar"); + + private boolean watchMode; + + public boolean isWatchMode() + { + return watchMode; + } + + public void setWatchMode(boolean aWatchMode) + { + watchMode = aWatchMode; + } +} diff --git a/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/WatchAnnotationTask.java b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/WatchAnnotationTask.java new file mode 100644 index 00000000000..f2b0562dd39 --- /dev/null +++ b/inception/inception-assistant/src/main/java/de/tudarmstadt/ukp/inception/assistant/sidebar/WatchAnnotationTask.java @@ -0,0 +1,218 @@ +/* + * 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.assistant.sidebar; + +import static de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasAccessMode.SHARED_READ_ONLY_ACCESS; +import static de.tudarmstadt.ukp.clarin.webanno.api.casstorage.CasUpgradeMode.AUTO_CAS_UPGRADE; +import static de.tudarmstadt.ukp.inception.assistant.model.MChatRoles.SYSTEM; +import static de.tudarmstadt.ukp.inception.scheduling.MatchResult.NO_MATCH; +import static de.tudarmstadt.ukp.inception.scheduling.MatchResult.QUEUE_THIS; +import static de.tudarmstadt.ukp.inception.support.json.JSONUtil.toPrettyJsonString; +import static de.tudarmstadt.ukp.inception.support.uima.ICasUtil.selectAnnotationByAddr; +import static java.lang.String.join; +import static java.util.Objects.requireNonNull; +import static org.apache.commons.lang3.StringUtils.normalizeSpace; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Objects; + +import org.apache.commons.lang3.Validate; +import org.apache.uima.jcas.tcas.Annotation; +import org.springframework.beans.factory.annotation.Autowired; + +import de.tudarmstadt.ukp.clarin.webanno.api.casstorage.session.CasStorageSession; +import de.tudarmstadt.ukp.clarin.webanno.model.SourceDocument; +import de.tudarmstadt.ukp.dkpro.core.api.segmentation.type.Sentence; +import de.tudarmstadt.ukp.inception.assistant.AssistantService; +import de.tudarmstadt.ukp.inception.assistant.model.MTextMessage; +import de.tudarmstadt.ukp.inception.documents.api.DocumentService; +import de.tudarmstadt.ukp.inception.rendering.vmodel.VID; +import de.tudarmstadt.ukp.inception.scheduling.MatchResult; +import de.tudarmstadt.ukp.inception.scheduling.MatchableTask; +import de.tudarmstadt.ukp.inception.scheduling.Task; +import de.tudarmstadt.ukp.inception.schema.api.AnnotationSchemaService; + +public class WatchAnnotationTask + extends Task + implements MatchableTask +{ + public static final String TYPE = "WatchAnnotationTask"; + private static final String ACTOR = "Annotation watcher"; + + private @Autowired AnnotationSchemaService schemaService; + private @Autowired AssistantService assistantService; + private @Autowired DocumentService documentService; + + private final SourceDocument document; + private final String dataOwner; + private final VID annotation; + + public WatchAnnotationTask(Builder> aBuilder) + { + super(aBuilder.withType(TYPE)); + + requireNonNull(getUser().orElse(null), "Session owner must be set"); + + document = aBuilder.document; + dataOwner = aBuilder.dataOwner; + annotation = aBuilder.annotation; + } + + @Override + public MatchResult matches(Task aTask) + { + if (aTask instanceof WatchAnnotationTask) { + if (Objects.equals(getProject().getId(), aTask.getProject().getId()) + && Objects.equals(getUser().get(), aTask.getUser().orElse(null))) { + return QUEUE_THIS; + } + } + + return NO_MATCH; + } + + @Override + public void execute() throws Exception + { + try (var session = CasStorageSession.open()) { + var cas = documentService.readAnnotationCas(document, dataOwner, AUTO_CAS_UPGRADE, + SHARED_READ_ONLY_ACCESS); + + var ann = selectAnnotationByAddr(cas, annotation.getId()); + if (ann == null) { + return; + } + + var contextSentence = cas.select(Sentence.class).covering(ann).nullOK().get(); + if (contextSentence == null) { + return; + } + + var instance = annotationToJson(ann, contextSentence); + + var checkQuestion = MTextMessage.builder() // + .withActor(ACTOR) // + .withRole(SYSTEM).internal().ephemeral() // + .withMessage(join("\n", // + "Is the following annotation correct or not. Answer true or false.", // + "\n", // + "```json", // + instance, // + "```")) // + .build(); + + var checkResult = assistantService.processInternalCallSync( + getUser().get().getUsername(), getProject(), BooleanQuestion.class, + checkQuestion); + + if (checkResult.payload().answer()) { + return; + } + + var inquiryContext = MTextMessage.builder() // + .withActor(ACTOR) // + .withRole(SYSTEM).internal().ephemeral() // + .withMessage(join("\n", // + "Your task is to advise the user about potential problems with the following annotation.", // + "Give one response per annotation.", // + "If expanding or reducing the span seems appropriate, mention that.", // + "Use markdown for formatting.", // + "", // + "```json", // + instance, // + "```")) // + .build(); + + assistantService.processAgentMessage(getUser().get().getUsername(), getProject(), + inquiryContext); + } + } + + private String annotationToJson(Annotation aAnnotation, Annotation aContext) throws IOException + { + var instance = new LinkedHashMap(); + + instance.put("span", normalizeSpace(aAnnotation.getCoveredText())); + + var docText = aAnnotation.getCAS().getDocumentText(); + var context = docText.substring(aContext.getBegin(), aAnnotation.getBegin()) // + + " " // + + aAnnotation.getCoveredText() // + + " " // + + docText.substring(aAnnotation.getEnd(), aContext.getEnd()); + instance.put("context", normalizeSpace(context)); + + var adapter = schemaService.findAdapter(getProject(), aAnnotation); + var attributes = new LinkedHashMap(); + for (var feature : adapter.listFeatures()) { + attributes.put(normalizeSpace(feature.getUiName()), + normalizeSpace(adapter.getFeatureValue(feature, aAnnotation))); + } + instance.put("annotation", attributes); + + return toPrettyJsonString(instance); + } + + private static record BooleanQuestion(boolean answer) {}; + + public static Builder> builder() + { + return new Builder<>(); + } + + public static class Builder> + extends Task.Builder + { + private SourceDocument document; + private String dataOwner; + private VID annotation; + + protected Builder() + { + } + + @SuppressWarnings("unchecked") + public T withAnnotation(VID aVid) + { + annotation = aVid; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T withDocument(SourceDocument aDocument) + { + document = aDocument; + return (T) this; + } + + @SuppressWarnings("unchecked") + public T withDataOwner(String aDataOwner) + { + dataOwner = aDataOwner; + return (T) this; + } + + public WatchAnnotationTask build() + { + Validate.notNull(project, "Parameter [project] must be specified"); + + return new WatchAnnotationTask(this); + } + } +} diff --git a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation/MetadataSuggestionSupport.java b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation/MetadataSuggestionSupport.java index 17f0ff3d9eb..7165656b014 100644 --- a/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation/MetadataSuggestionSupport.java +++ b/inception/inception-layer-docmetadata/src/main/java/de/tudarmstadt/ukp/inception/ui/core/docanno/recommendation/MetadataSuggestionSupport.java @@ -226,9 +226,9 @@ private void hideSpanSuggestionsThatMatchAnnotations(boolean singleton, sugGroup.hideAll(FLAG_OVERLAP); } else { - for (var sug : sugGroup) { - if (label.equals(sug.getLabel())) { - sug.hide(FLAG_OVERLAP); + for (var suggestion : sugGroup) { + if (label.equals(suggestion.getLabel())) { + suggestion.hide(FLAG_OVERLAP); } } } diff --git a/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/TrainingInstance.java b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/TrainingInstance.java new file mode 100644 index 00000000000..559f05489a8 --- /dev/null +++ b/inception/inception-recommendation-api/src/main/java/de/tudarmstadt/ukp/inception/recommendation/api/recommender/TrainingInstance.java @@ -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.recommender; + +public interface TrainingInstance +{ + +} diff --git a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/llm/InteractiveRecommenderSidebar.java b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/llm/InteractiveRecommenderSidebar.java index 9bc58109d97..d495456021c 100644 --- a/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/llm/InteractiveRecommenderSidebar.java +++ b/inception/inception-recommendation/src/main/java/de/tudarmstadt/ukp/inception/recommendation/sidebar/llm/InteractiveRecommenderSidebar.java @@ -383,7 +383,7 @@ private List listLayers(Recommender aRecommender) private void execute(AjaxRequestTarget aTarget, Form aForm) throws Exception { var sessionOwner = userService.getCurrentUser(); - var state = getAnnotationPage().getModelObject(); + var state = getModelObject(); var document = state.getDocument(); var dataOwner = state.getUser().getUsername(); var rec = aForm.getModelObject(); diff --git a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/MatchResult.java b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/MatchResult.java index 6e5f8dafbdf..569f7449f1c 100644 --- a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/MatchResult.java +++ b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/MatchResult.java @@ -34,5 +34,11 @@ public enum MatchResult * Discard the incoming task if it matches an already enqueued task. If a matching task is * already scheduled or running, then queue the incoming task. */ - DISCARD_OR_QUEUE_THIS; + DISCARD_OR_QUEUE_THIS, + + /** + * Queue this task. It will be run immediately or after other matching tasks have been run. It + * will not run parallel to matching tasks. + */ + QUEUE_THIS; } diff --git a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/SchedulingServiceImpl.java b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/SchedulingServiceImpl.java index 558e86fbf00..11f7199f1f2 100644 --- a/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/SchedulingServiceImpl.java +++ b/inception/inception-scheduling/src/main/java/de/tudarmstadt/ukp/inception/scheduling/SchedulingServiceImpl.java @@ -247,6 +247,10 @@ public synchronized void enqueue(Task aTask) // the incoming task supersedes them). tasksToUnqueue.add(enqueuedTask); break; + case QUEUE_THIS: + // Queue this task to be run potentially after other matching tasks have been + // completed + break; case NO_MATCH: // Ignore break; diff --git a/inception/inception-sharing/src/main/java/de/tudarmstadt/ukp/inception/sharing/project/InviteProjectSettingsPanel.java b/inception/inception-sharing/src/main/java/de/tudarmstadt/ukp/inception/sharing/project/InviteProjectSettingsPanel.java index df00cd214f4..7b28421734e 100644 --- a/inception/inception-sharing/src/main/java/de/tudarmstadt/ukp/inception/sharing/project/InviteProjectSettingsPanel.java +++ b/inception/inception-sharing/src/main/java/de/tudarmstadt/ukp/inception/sharing/project/InviteProjectSettingsPanel.java @@ -17,6 +17,7 @@ */ package de.tudarmstadt.ukp.inception.sharing.project; +import static de.tudarmstadt.ukp.inception.support.lambda.HtmlElementEvents.CHANGE_EVENT; import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhen; import static de.tudarmstadt.ukp.inception.support.lambda.LambdaBehavior.visibleWhenNot; import static java.util.Arrays.asList; @@ -103,14 +104,14 @@ protected void onDisabled(ComponentTag tag) detailsForm.add(new CheckBox("guestAccessible").setOutputMarkupId(true) .add(visibleWhen(() -> inviteServiceProperties.isGuestsEnabled())) - .add(new LambdaAjaxFormSubmittingBehavior("change", _t -> _t.add(this)))); + .add(new LambdaAjaxFormSubmittingBehavior(CHANGE_EVENT, _t -> _t.add(this)))); DropDownChoice askForEMail = new DropDownChoice<>("askForEMail", asList(Mandatoriness.values()), new EnumChoiceRenderer<>(this)); askForEMail.setOutputMarkupId(true); askForEMail.add(visibleWhen(() -> inviteServiceProperties.isGuestsEnabled() && invite.map(ProjectInvite::isGuestAccessible).orElse(false).getObject())); - askForEMail.add(new LambdaAjaxFormSubmittingBehavior("change", _t -> _t.add(this))); + askForEMail.add(new LambdaAjaxFormSubmittingBehavior(CHANGE_EVENT, _t -> _t.add(this))); detailsForm.add(askForEMail); detailsForm.add(new TextField<>("userIdPlaceholder") diff --git a/inception/inception-support/src/main/java/de/tudarmstadt/ukp/inception/support/json/JSONUtil.java b/inception/inception-support/src/main/java/de/tudarmstadt/ukp/inception/support/json/JSONUtil.java index 89537a7cf60..ea6b4bf1ea0 100644 --- a/inception/inception-support/src/main/java/de/tudarmstadt/ukp/inception/support/json/JSONUtil.java +++ b/inception/inception-support/src/main/java/de/tudarmstadt/ukp/inception/support/json/JSONUtil.java @@ -93,12 +93,14 @@ public static String toJsonString(Object aObject) throws IOException public static String toJsonString(ObjectMapper aMapper, boolean aPretty, Object aObject) throws IOException { - StringWriter out = new StringWriter(); + var out = new StringWriter(); + + var jsonGenerator = aMapper.getFactory().createGenerator(out); - JsonGenerator jsonGenerator = aMapper.getFactory().createGenerator(out); if (aPretty) { - jsonGenerator.setPrettyPrinter(new DefaultPrettyPrinter() - .withObjectIndenter(new DefaultIndenter().withLinefeed("\n"))); + jsonGenerator.setPrettyPrinter(new DefaultPrettyPrinter() // + .withObjectIndenter(new DefaultIndenter() // + .withLinefeed("\n"))); } jsonGenerator.writeObject(aObject); @@ -110,9 +112,8 @@ public static T fromJsonString(Class aClass, String aJSON) throws IOExcep if (aJSON == null) { return null; } - else { - return getObjectMapper().readValue(aJSON, aClass); - } + + return getObjectMapper().readValue(aJSON, aClass); } public static T fromValidatedJsonString(Class aClass, String aJSON, JsonSchema aSchema)