2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.voice.actiontemplatehli.internal;
15 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.DYNAMIC_PLACEHOLDER;
16 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.DYNAMIC_PLACEHOLDER_SYMBOL;
17 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.GROUP_LABEL_PLACEHOLDER_SYMBOL;
18 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.ITEM_LABEL_PLACEHOLDER;
19 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.ITEM_LABEL_PLACEHOLDER_SYMBOL;
20 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.ITEM_OPTION_PLACEHOLDER;
21 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.ITEM_OPTION_PLACEHOLDER_SYMBOL;
22 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.NER_FOLDER;
23 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.NLP_FOLDER;
24 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.POS_FOLDER;
25 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.SERVICE_CATEGORY;
26 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.SERVICE_ID;
27 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.SERVICE_NAME;
28 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.SERVICE_PID;
29 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.STATE_PLACEHOLDER;
30 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.STATE_PLACEHOLDER_SYMBOL;
31 import static org.openhab.voice.actiontemplatehli.internal.ActionTemplateInterpreterConstants.TYPE_ACTION_CONFIGS_FOLDER;
33 import java.awt.Color;
35 import java.io.FileInputStream;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.nio.file.Path;
39 import java.util.ArrayList;
40 import java.util.Arrays;
41 import java.util.Collections;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Locale;
46 import java.util.Objects;
48 import java.util.regex.Pattern;
49 import java.util.stream.Collectors;
50 import java.util.stream.Stream;
52 import org.eclipse.jdt.annotation.NonNullByDefault;
53 import org.eclipse.jdt.annotation.Nullable;
54 import org.openhab.core.common.registry.RegistryChangeListener;
55 import org.openhab.core.config.core.ConfigurableService;
56 import org.openhab.core.config.core.Configuration;
57 import org.openhab.core.events.EventPublisher;
58 import org.openhab.core.items.GroupItem;
59 import org.openhab.core.items.Item;
60 import org.openhab.core.items.ItemRegistry;
61 import org.openhab.core.items.Metadata;
62 import org.openhab.core.items.MetadataKey;
63 import org.openhab.core.items.MetadataRegistry;
64 import org.openhab.core.items.events.ItemEventFactory;
65 import org.openhab.core.library.types.HSBType;
66 import org.openhab.core.library.types.OnOffType;
67 import org.openhab.core.library.types.OpenClosedType;
68 import org.openhab.core.library.types.StringType;
69 import org.openhab.core.types.Command;
70 import org.openhab.core.types.State;
71 import org.openhab.core.types.TypeParser;
72 import org.openhab.core.types.UnDefType;
73 import org.openhab.core.voice.text.HumanLanguageInterpreter;
74 import org.openhab.core.voice.text.InterpretationException;
75 import org.openhab.voice.actiontemplatehli.internal.configuration.ActionTemplateConfiguration;
76 import org.openhab.voice.actiontemplatehli.internal.configuration.ActionTemplateGroupTargets;
77 import org.openhab.voice.actiontemplatehli.internal.configuration.ActionTemplatePlaceholder;
78 import org.osgi.framework.Constants;
79 import org.osgi.service.component.annotations.Activate;
80 import org.osgi.service.component.annotations.Component;
81 import org.osgi.service.component.annotations.Deactivate;
82 import org.osgi.service.component.annotations.Modified;
83 import org.osgi.service.component.annotations.Reference;
84 import org.slf4j.Logger;
85 import org.slf4j.LoggerFactory;
87 import opennlp.tools.dictionary.Dictionary;
88 import opennlp.tools.lemmatizer.DictionaryLemmatizer;
89 import opennlp.tools.lemmatizer.Lemmatizer;
90 import opennlp.tools.lemmatizer.LemmatizerME;
91 import opennlp.tools.lemmatizer.LemmatizerModel;
92 import opennlp.tools.namefind.DictionaryNameFinder;
93 import opennlp.tools.namefind.NameFinderME;
94 import opennlp.tools.namefind.TokenNameFinderModel;
95 import opennlp.tools.postag.POSDictionary;
96 import opennlp.tools.postag.POSModel;
97 import opennlp.tools.postag.POSTaggerME;
98 import opennlp.tools.tokenize.SimpleTokenizer;
99 import opennlp.tools.tokenize.Tokenizer;
100 import opennlp.tools.tokenize.TokenizerME;
101 import opennlp.tools.tokenize.TokenizerModel;
102 import opennlp.tools.tokenize.WhitespaceTokenizer;
103 import opennlp.tools.util.Span;
104 import opennlp.tools.util.StringList;
107 * The {@link ActionTemplateInterpreter} is a configurable interpreter powered by OpenNLP
109 * @author Miguel Álvarez - Initial contribution
112 @Component(configurationPid = SERVICE_PID, property = Constants.SERVICE_PID + "=" + SERVICE_PID)
113 @ConfigurableService(category = SERVICE_CATEGORY, label = SERVICE_NAME, description_uri = SERVICE_CATEGORY + ":"
115 public class ActionTemplateInterpreter implements HumanLanguageInterpreter {
117 Logger logger = LoggerFactory.getLogger(ActionTemplateInterpreter.class);
118 createFolder(logger, NLP_FOLDER);
119 createFolder(logger, NER_FOLDER);
120 createFolder(logger, POS_FOLDER);
121 createFolder(logger, TYPE_ACTION_CONFIGS_FOLDER);
123 private static final Pattern COLOR_HEX_PATTERN = Pattern.compile("^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$");
124 private final Logger logger = LoggerFactory.getLogger(ActionTemplateInterpreter.class);
125 private final ItemRegistry itemRegistry;
126 private final MetadataRegistry metadataRegistry;
127 private final EventPublisher eventPublisher;
128 private ActionTemplateInterpreterConfiguration config = new ActionTemplateInterpreterConfiguration();
129 private Tokenizer tokenizer = WhitespaceTokenizer.INSTANCE;
130 private List<String> optionalLanguageTags = List.of();
132 private NLPItemMaps nlpItemMaps;
134 private final RegistryChangeListener<Item> registryChangeListener = new RegistryChangeListener<>() {
136 public void added(Item element) {
141 public void removed(Item element) {
146 public void updated(Item oldElement, Item element) {
152 public ActionTemplateInterpreter(@Reference ItemRegistry itemRegistry, @Reference MetadataRegistry metadataRegistry,
153 @Reference EventPublisher eventPublisher) {
154 this.itemRegistry = itemRegistry;
155 this.metadataRegistry = metadataRegistry;
156 this.eventPublisher = eventPublisher;
157 itemRegistry.addRegistryChangeListener(registryChangeListener);
161 protected void activate(Map<String, Object> config) {
166 protected void modified(Map<String, Object> config) {
167 this.config = new Configuration(config).as(ActionTemplateInterpreterConfiguration.class);
172 protected void deactivate() {
173 itemRegistry.removeRegistryChangeListener(registryChangeListener);
177 public String getId() {
182 public String getLabel(@Nullable Locale locale) {
187 public @Nullable String getGrammar(@Nullable Locale locale, @Nullable String s) {
192 public Set<Locale> getSupportedLocales() {
198 public Set<String> getSupportedGrammarFormats() {
203 public String interpret(Locale locale, String words) throws InterpretationException {
204 if (words.isEmpty()) {
205 throw new InterpretationException(config.unhandledMessage);
208 var finalWords = config.lowerText ? words.toLowerCase(locale) : words;
209 var info = getNLPInfo(finalWords);
210 if (info.tokens.length == 0) {
211 logger.debug("no tokens produced; aborting");
212 throw new InterpretationException(config.failureMessage);
214 String response = processAction(finalWords,
215 checkActionConfigs(finalWords, info.tokens, info.tags, info.lemmas));
216 if (response == null) {
217 logger.debug("silent mode; no response");
220 logger.debug("response: {}", response);
222 } catch (IOException e) {
223 logger.debug("IOException while interpreting: {}", e.getMessage(), e);
224 var message = e.getMessage();
225 throw new InterpretationException(message != null ? message : "Unknown error");
226 } catch (RuntimeException e) {
227 var message = e.getMessage();
228 logger.debug("RuntimeException while interpreting: {}", e.getMessage(), e);
229 throw new InterpretationException(message != null ? message : "Unknown error");
233 private @Nullable String processAction(String words, @Nullable NLPInterpretationResult result)
234 throws InterpretationException, IOException {
235 if (result != null) {
236 if (!result.actionConfig.read) {
237 return sendItemCommand(result.targetItem, words, result.actionConfig, result.placeholderValues);
239 return readItemState(result.targetItem, result.actionConfig);
242 throw new InterpretationException(config.unhandledMessage);
246 private NLPInfo getNLPInfo(String text) throws IOException {
247 logger.debug("Processing: '{}'", text);
248 var tokens = tokenizeText(text);
249 var tags = languagePOSTagging(tokens);
250 var lemmas = languageLemmatize(tokens, tags);
251 logger.debug("tokens: {}", List.of(tokens));
252 logger.debug("tags: {}", List.of(tags));
253 logger.debug("lemmas: {}", List.of(lemmas));
254 return new NLPInfo(tokens, lemmas, tags);
257 private @Nullable NLPInterpretationResult checkActionConfigs(String text, String[] tokens, String[] tags,
258 String[] lemmas) throws IOException {
259 // item defined actions have priority over type defined actions
260 var result = checkItemActions(text, tokens, tags, lemmas);
261 if (result != null) {
264 return checkTypeActionsConfigs(text, tokens, tags, lemmas);
267 private @Nullable NLPInterpretationResult checkItemActions(String text, String[] tokens, String[] tags,
268 String[] lemmas) throws IOException {
269 // Check item with action config
270 var itemsWithActions = getItemsWithActionConfigs();
271 Item targetItem = null;
272 ActionTemplateConfiguration targetActionConfig = null;
273 // store data to restore placeholder values
274 List<NLPPlaceholderData> placeholderValues = null;
275 // store span of dynamic placeholder, to invalidate others
276 Span dynamicSpan = null;
278 for (var entry : itemsWithActions.entrySet()) {
279 var actionConfigs = entry.getValue();
280 for (var actionConfig : actionConfigs) {
281 var templates = Arrays.stream(actionConfig.template.split(";")).map(String::trim)
282 .collect(Collectors.toList());
283 for (var template : templates) {
284 List<NLPPlaceholderData> currentPlaceholderValues = new ArrayList<>();
285 var currentItem = entry.getKey();
286 var scoreResult = getScoreWithPlaceholders(text, currentItem, actionConfig.memberTargets,
287 actionConfig.read, tokens, tags, lemmas, actionConfig, template, currentPlaceholderValues);
288 if (scoreResult.score != 0 && scoreResult.score == matchScore) {
289 if (targetItem == currentItem) {
291 "multiple alternative templates for item '{}' has the same score, '{}' can be removed",
292 targetItem.getName(), template);
295 "multiple templates with same score for items '{}' and '{}', the action with template '{}' can be removed",
296 targetItem.getName(), currentItem.getName(), template);
299 if (scoreResult.score > matchScore) {
300 targetItem = currentItem;
301 targetActionConfig = actionConfig;
302 placeholderValues = currentPlaceholderValues;
303 matchScore = scoreResult.score;
304 dynamicSpan = scoreResult.dynamicSpan;
309 if (targetItem != null && targetActionConfig != null && placeholderValues != null) {
310 if (dynamicSpan != null) {
311 placeholderValues = updatePlaceholderValues(text, tokens, placeholderValues, dynamicSpan);
313 return new NLPInterpretationResult(targetItem, targetActionConfig, placeholderValues.stream()
314 .collect(Collectors.toMap(i -> i.placeholderName, i -> i.placeholderValue)));
319 private @Nullable NLPInterpretationResult checkTypeActionsConfigs(String text, String[] tokens, String[] tags,
320 String[] lemmas) throws IOException {
321 // Check item command
322 var itemLabelSpans = nerItemLabels(tokens);
323 logger.debug("itemLabelSpans: {}", List.of(itemLabelSpans));
324 if (itemLabelSpans.length == 0) {
325 logger.debug("No item labels found!");
328 Item finalTargetItem = null;
329 ActionTemplateConfiguration targetActionConfig = null;
330 // store data to restore placeholder values
331 List<NLPPlaceholderData> placeholderValues = null;
332 // store span of dynamic placeholder, to invalidate others
333 Span dynamicSpan = null;
335 // iterate itemLabelSpan to score the templates with each of them
336 for (var itemLabelSpan : itemLabelSpans) {
337 var labelTokens = getTargetItemTokens(tokens, itemLabelSpan);
338 var targetItem = getTargetItemByLabelTokens(labelTokens);
339 if (targetItem == null) {
342 var tokensWithGenericLabel = replacePlaceholder(text, tokens, itemLabelSpan, ITEM_LABEL_PLACEHOLDER, null,
344 var lemmasWithGenericLabel = lemmas.length > 0
345 ? replacePlaceholder(text, lemmas, itemLabelSpan, ITEM_LABEL_PLACEHOLDER, null, null)
347 var tagsWithGenericLabel = tags.length > 0
348 ? replacePlaceholder(text, tags, itemLabelSpan, ITEM_LABEL_PLACEHOLDER, null, null)
350 logger.debug("Target item {}", targetItem.getName());
351 // load templates defined for this item type
352 var typeActionConfigs = getTypeActionConfigs(targetItem.getType());
353 for (var actionConfig : typeActionConfigs) {
354 // check required item tags
355 if (actionConfig.requiredItemTags.length != 0) {
356 var itemLabels = targetItem.getTags();
357 if (!Arrays.stream(actionConfig.requiredItemTags).allMatch(itemLabels::contains)) {
358 logger.debug("action '{}' skipped, tags constrain '{}'", actionConfig.template,
359 List.of(actionConfig.requiredItemTags));
363 var templates = Arrays.stream(actionConfig.template.split(";")).map(String::trim)
364 .collect(Collectors.toList());
365 for (var template : templates) {
366 var replacedValues = new ArrayList<NLPPlaceholderData>();
367 var scoreResult = getScoreWithPlaceholders(text, targetItem, actionConfig.memberTargets,
368 actionConfig.read, tokensWithGenericLabel, tagsWithGenericLabel, lemmasWithGenericLabel,
369 actionConfig, template, replacedValues);
370 if (scoreResult.score != 0 && scoreResult.score == matchScore
371 && actionConfig.requiredItemTags.length == targetActionConfig.requiredItemTags.length) {
372 if (targetActionConfig == actionConfig) {
374 "multiple alternative templates with same score, you can remove the alternative '{}'",
378 "multiple templates with same score, the action with template '{}' can be removed",
382 // for rules with same score the one with more restrictions have prevalence
383 if (scoreResult.score > matchScore || (scoreResult.score == matchScore && targetActionConfig != null
384 && actionConfig.requiredItemTags.length > targetActionConfig.requiredItemTags.length)) {
385 finalTargetItem = targetItem;
386 placeholderValues = replacedValues;
387 targetActionConfig = actionConfig;
388 matchScore = scoreResult.score;
389 dynamicSpan = scoreResult.dynamicSpan;
394 if (finalTargetItem != null && targetActionConfig != null && placeholderValues != null) {
395 if (dynamicSpan != null) {
396 placeholderValues = updatePlaceholderValues(text, tokens, placeholderValues, dynamicSpan);
398 return NLPInterpretationResult.from(finalTargetItem, targetActionConfig, placeholderValues);
403 private List<NLPPlaceholderData> updatePlaceholderValues(String text, String[] tokens,
404 List<NLPPlaceholderData> placeholderValues, Span dynamicSpan) {
405 // we should clean up placeholder values detected inside the dynamic template
406 var validPlaceholderValues = placeholderValues.stream().filter(i -> !dynamicSpan.contains(i.placeholderSpan))
407 .collect(Collectors.toList());
408 // add dynamic content to values
409 validPlaceholderValues.add(new NLPPlaceholderData(DYNAMIC_PLACEHOLDER,
410 detokenize(Arrays.copyOfRange(tokens, dynamicSpan.getStart(), dynamicSpan.getEnd()), text),
412 return validPlaceholderValues;
415 private Set<Item> getMembersByTypeRecursive(GroupItem group, String itemType, String[] requiredMemberTags) {
416 Stream<Item> targetMembersStream = getMembersByType(group, itemType, requiredMemberTags).stream();
417 var childGroups = getMembersByType(group, "Group", new String[] {});
418 for (var childGroup : childGroups) {
419 targetMembersStream = Stream.concat(targetMembersStream,
420 getMembersByTypeRecursive((GroupItem) childGroup, itemType, requiredMemberTags).stream());
422 return targetMembersStream.collect(Collectors.toUnmodifiableSet());
425 private State mergeSwitchMembersState(GroupItem group, String[] requiredMemberTags, boolean recursive) {
426 var result = OnOffType.OFF;
427 var targetMembers = recursive ? getMembersByTypeRecursive(group, "Switch", requiredMemberTags)
428 : getMembersByType(group, "Switch", requiredMemberTags);
429 for (var member : targetMembers) {
430 if (UnDefType.UNDEF.equals(member.getState())) {
431 return UnDefType.UNDEF;
433 if (UnDefType.NULL.equals(member.getState())) {
434 return UnDefType.NULL;
436 if (OnOffType.ON.equals(member.getState())) {
437 result = OnOffType.ON;
443 private State mergeContactMembersState(GroupItem group, String[] requiredMemberTags, boolean recursive) {
444 var result = OpenClosedType.CLOSED;
445 var targetMembers = recursive ? getMembersByTypeRecursive(group, "Contact", requiredMemberTags)
446 : getMembersByType(group, "Contact", requiredMemberTags);
447 for (var member : targetMembers) {
448 if (UnDefType.UNDEF.equals(member.getState())) {
449 return UnDefType.UNDEF;
451 if (UnDefType.NULL.equals(member.getState())) {
452 return UnDefType.NULL;
454 if (OpenClosedType.OPEN.equals(member.getState())) {
455 result = OpenClosedType.OPEN;
461 private String readItemState(Item targetItem, ActionTemplateConfiguration actionConfigMatch)
462 throws IOException, InterpretationException {
463 var memberTargets = actionConfigMatch.memberTargets;
465 String itemLabel = targetItem.getLabel();
466 String groupLabel = null;
467 Item finalTargetItem = targetItem;
468 if ("Group".equals(finalTargetItem.getType()) && memberTargets != null) {
469 if (memberTargets.mergeState && memberTargets.itemName.isEmpty() && !memberTargets.itemType.isEmpty()) {
470 // handle states that can be merged
471 switch (memberTargets.itemType) {
473 state = mergeSwitchMembersState((GroupItem) finalTargetItem, memberTargets.requiredItemTags,
474 memberTargets.recursive).toFullString();
477 state = mergeContactMembersState((GroupItem) finalTargetItem, memberTargets.requiredItemTags,
478 memberTargets.recursive).toFullString();
481 logger.warn("state merge is not available for members of type {}", memberTargets.itemType);
482 throw new InterpretationException(config.failureMessage);
486 Set<Item> targetMembers = getTargetMembers((GroupItem) finalTargetItem, memberTargets);
487 if (!targetMembers.isEmpty()) {
488 if (targetMembers.size() > 1) {
489 logger.warn("read action matches {} item members inside a group, using the first one",
490 targetMembers.size());
492 var targetMember = targetMembers.iterator().next();
493 // only one target in the group, adding groupLabel placeholder value
494 groupLabel = itemLabel;
495 itemLabel = targetMember.getLabel();
496 state = targetMember.getState().toFullString();
497 finalTargetItem = targetMember;
499 logger.warn("configured targetMembers were not found in group '{}'", finalTargetItem.getName());
500 throw new InterpretationException(config.failureMessage);
505 state = finalTargetItem.getState().toFullString();
507 var statePlaceholder = actionConfigMatch.placeholders.stream().filter(p -> p.label.equals(STATE_PLACEHOLDER))
509 var itemState = state;
510 if (statePlaceholder.isPresent()) {
511 state = applyPOSTransformation(state, statePlaceholder.get());
513 var template = actionConfigMatch.value;
514 if (!actionConfigMatch.emptyValue.isEmpty() && (state.isEmpty()
515 || (UnDefType.UNDEF.toFullString().equals(state) || UnDefType.NULL.toFullString().equals(state)))) {
516 // use alternative template for empty values
517 template = actionConfigMatch.emptyValue;
519 if (template instanceof String) {
520 String templateText = (String) template;
521 if (templateText.contains(ITEM_OPTION_PLACEHOLDER)) {
522 var itemOptionPlaceholder = getItemOptionPlaceholder(finalTargetItem, true, null);
523 if (itemOptionPlaceholder != null) {
524 itemState = applyPOSTransformation(itemState, itemOptionPlaceholder);
527 return templateText.replace(STATE_PLACEHOLDER_SYMBOL, state)
528 .replace(ITEM_OPTION_PLACEHOLDER_SYMBOL, itemState)
529 .replace(ITEM_LABEL_PLACEHOLDER_SYMBOL, itemLabel != null ? itemLabel : "")
530 .replace(GROUP_LABEL_PLACEHOLDER_SYMBOL, groupLabel != null ? groupLabel : "");
535 private NLPTokenComparisonResult getScoreWithPlaceholders(String text, Item targetItem,
536 @Nullable ActionTemplateGroupTargets targetMembers, boolean isRead, String[] tokens, String[] tags,
537 String[] lemmas, ActionTemplateConfiguration actionConfiguration, String template,
538 List<NLPPlaceholderData> placeholderValues) throws IOException {
539 var placeholders = new ArrayList<>(actionConfiguration.placeholders);
540 var finalTokens = tokens;
541 var finalLemmas = lemmas;
542 var finalTags = tags;
543 if (template.contains(ITEM_OPTION_PLACEHOLDER_SYMBOL)) {
544 var itemOptionPlaceholder = getItemOptionPlaceholder(targetItem, isRead, targetMembers);
545 if (itemOptionPlaceholder == null) {
546 return NLPTokenComparisonResult.ZERO;
548 placeholders.add(itemOptionPlaceholder);
550 for (var placeholder : placeholders) {
551 if (actionConfiguration.read && placeholder.label.equals(STATE_PLACEHOLDER)) {
552 // This placeholder is reserved on read mode should not be replaced now
555 if (placeholder.label.equals(DYNAMIC_PLACEHOLDER)) {
556 logger.warn("the name {} is reserved for the dynamic placeholder", DYNAMIC_PLACEHOLDER);
559 var nerStaticValues = placeholder.nerStaticValues;
560 var nerFile = placeholder.nerFile;
562 Map<String[], String> possibleValuesByTokensMap = null;
563 if (nerStaticValues != null) {
564 possibleValuesByTokensMap = getStringsByTokensMap(nerStaticValues);
565 nerSpans = nerValues(finalTokens, possibleValuesByTokensMap.keySet().toArray(String[][]::new),
567 } else if (nerFile != null) {
568 nerSpans = nerWithFile(finalTokens, nerFile);
570 logger.warn("Placeholder {} could not be applied due to missing ner config", placeholder.label);
573 for (Span nerSpan : nerSpans) {
574 var placeholderName = placeholder.label;
575 finalTokens = replacePlaceholder(text, finalTokens, nerSpan, placeholderName, placeholderValues,
576 possibleValuesByTokensMap);
577 if (finalLemmas.length > 0) {
578 finalLemmas = replacePlaceholder(text, finalLemmas, nerSpan, placeholderName, null, null);
580 if (finalTags.length > 0) {
581 finalTags = replacePlaceholder(text, finalTags, nerSpan, placeholderName, null, null);
585 return getScore(finalTokens, finalTags, finalLemmas, actionConfiguration, template);
588 private NLPTokenComparisonResult getScore(String[] tokens, String[] tags, String[] lemmas,
589 ActionTemplateConfiguration actionConfiguration, String template) {
590 switch (actionConfiguration.type) {
592 String[] tokensTemplate = splitString(template, "\\s");
593 var scoreByTokens = compareTokens(tokens, tags, tokensTemplate);
594 logger.debug("tokens '{}' score: {}", List.of(tokensTemplate), scoreByTokens.score);
595 return scoreByTokens;
597 String[] lemmasTemplate = splitString(template, "\\s");
598 var scoreByLemmas = compareTokens(lemmas, tags, lemmasTemplate);
599 logger.debug("lemmas '{}' score: {}", List.of(lemmasTemplate), scoreByLemmas.score);
600 return scoreByLemmas;
602 logger.warn("Unsupported template type '{}'", actionConfiguration.type);
603 return NLPTokenComparisonResult.ZERO;
607 private @Nullable String sendItemCommand(Item item, String text, ActionTemplateConfiguration actionConfiguration,
608 Map<String, String> placeholderValues) throws IOException, InterpretationException {
609 Object valueTemplate = actionConfiguration.value;
610 boolean silent = actionConfiguration.silent;
611 String replacedValue = null;
612 Command command = null;
613 // Special type handling
614 switch (item.getType()) {
616 if (valueTemplate instanceof String) {
617 replacedValue = templatePlaceholders((String) valueTemplate, item, placeholderValues,
618 actionConfiguration.placeholders);
619 if (COLOR_HEX_PATTERN.matcher(replacedValue).matches()) {
620 Color rgb = Color.decode(replacedValue);
622 command = HSBType.fromRGB(rgb.getRed(), rgb.getGreen(), rgb.getBlue());
623 } catch (NumberFormatException e) {
624 logger.warn("Unable to parse value '{}' as color", replacedValue);
625 throw new InterpretationException(config.failureMessage);
631 var groupItem = (GroupItem) item;
632 var memberTargetsConfig = actionConfiguration.memberTargets;
633 if (memberTargetsConfig != null) {
634 Set<Item> targetMembers = getTargetMembers(groupItem, memberTargetsConfig);
635 logger.debug("{} target members were found in group {}", targetMembers.size(), groupItem.getName());
636 if (!targetMembers.isEmpty()) {
637 // swap the command target by the matched members
639 boolean groupsilent = true;
640 for (var targetMember : targetMembers) {
641 var response = sendItemCommand(targetMember, text, actionConfiguration, placeholderValues);
642 if (config.failureMessage.equals(response)) {
645 if (response != null) {
649 return ok ? (groupsilent ? null : config.commandSentMessage) : config.failureMessage;
651 logger.warn("configured targetMembers were not found in group '{}'", groupItem.getName());
652 throw new InterpretationException(config.failureMessage);
657 if (command == null) {
659 var objectValue = actionConfiguration.value;
660 if (objectValue != null) {
661 if (replacedValue == null) {
662 var stringValue = String.valueOf(objectValue);
663 replacedValue = templatePlaceholders(stringValue, item, placeholderValues,
664 actionConfiguration.placeholders);
666 command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), replacedValue);
667 } else if ("String".equals(item.getType())) {
668 // We interpret processing will continue in a rule
670 command = new StringType(text);
673 if (command == null) {
674 logger.warn("Command '{}' is not valid for item '{}'.", actionConfiguration.value, item.getName());
675 throw new InterpretationException(config.failureMessage);
677 eventPublisher.post(ItemEventFactory.createCommandEvent(item.getName(), command));
679 // when silent mode give no result
682 return config.commandSentMessage;
686 private @Nullable ActionTemplatePlaceholder getItemOptionPlaceholder(Item targetItem, boolean isRead,
687 @Nullable ActionTemplateGroupTargets memberTargets) {
688 if ("Group".equals(targetItem.getType()) && memberTargets != null) {
689 var targetMembers = getTargetMembers((GroupItem) targetItem, memberTargets);
690 logger.debug("{} target members were found in group {}", targetMembers.size(), targetItem.getName());
691 if (!targetMembers.isEmpty()) {
692 return targetMembers.stream().map(member -> getItemOptionPlaceholder(member, isRead, null))
693 .reduce(ActionTemplatePlaceholder.withLabel(ITEM_OPTION_PLACEHOLDER), (a, b) -> {
694 a.nerStaticValues = a.nerStaticValues != null
695 ? Stream.concat(Arrays.stream(a.nerStaticValues), Arrays.stream(b.nerStaticValues))
696 .distinct().toArray(String[]::new)
702 var cmdDescription = targetItem.getCommandDescription();
703 var stateDescription = targetItem.getStateDescription();
704 var itemOptionPlaceholder = ActionTemplatePlaceholder.withLabel(ITEM_OPTION_PLACEHOLDER);
705 if (!isRead && cmdDescription != null) {
706 itemOptionPlaceholder.nerStaticValues = cmdDescription.getCommandOptions().stream()
707 .map(option -> option.getLabel() != null ? option.getLabel() : option.getCommand())
708 .filter(Objects::nonNull).toArray(String[]::new);
709 itemOptionPlaceholder.posStaticValues = cmdDescription.getCommandOptions().stream()
710 .collect(Collectors.toMap(
711 option -> option.getLabel() != null ? option.getLabel().replace(" ", "__")
712 : option.getCommand().replace(" ", "__"),
713 option -> option.getCommand().replace(" ", "__")));
714 return itemOptionPlaceholder;
715 } else if (stateDescription != null) {
716 itemOptionPlaceholder.nerStaticValues = stateDescription.getOptions().stream()
717 .map(option -> option.getLabel() != null ? option.getLabel() : option.getValue())
718 .filter(Objects::nonNull).toArray(String[]::new);
720 itemOptionPlaceholder.posStaticValues = stateDescription.getOptions().stream()
721 .collect(Collectors.toMap(option -> option.getValue().replace(" ", "__"),
722 option -> option.getLabel() != null ? option.getLabel().replace(" ", "__")
723 : option.getValue().replace(" ", "__")));
725 itemOptionPlaceholder.posStaticValues = stateDescription.getOptions().stream()
726 .collect(Collectors.toMap(
727 option -> option.getLabel() != null ? option.getLabel().replace(" ", "__")
728 : option.getValue().replace(" ", "__"),
729 option -> option.getValue().replace(" ", "__")));
731 return itemOptionPlaceholder;
734 "'{}' is the target item for an action that uses the '{}' placeholder but hasn't got any state/command description",
735 targetItem.getName(), ITEM_OPTION_PLACEHOLDER_SYMBOL);
739 private Set<Item> getTargetMembers(GroupItem groupItem, ActionTemplateGroupTargets memberTargets) {
740 var childName = memberTargets.itemName;
741 if (!childName.isEmpty()) {
742 return groupItem.getMembers(i -> i.getName().equals(childName));
744 var itemType = memberTargets.itemType;
745 var requiredItemTags = memberTargets.requiredItemTags;
746 if (!itemType.isEmpty()) {
747 return memberTargets.recursive ? getMembersByTypeRecursive(groupItem, itemType, requiredItemTags)
748 : getMembersByType(groupItem, itemType, requiredItemTags);
753 private Set<Item> getMembersByType(GroupItem groupItem, String itemType, String[] requiredItemTags) {
754 return groupItem.getMembers(i -> i.getType().equals(itemType)
755 && (requiredItemTags.length == 0 || Arrays.stream(requiredItemTags).allMatch(i.getTags()::contains)));
758 private String templatePlaceholders(String text, Item targetItem, Map<String, String> placeholderValues,
759 List<ActionTemplatePlaceholder> placeholders) throws IOException {
760 var placeholdersCopy = new ArrayList<>(placeholders);
761 if (placeholderValues.containsKey(ITEM_OPTION_PLACEHOLDER)) {
762 var itemOptionPlaceholder = getItemOptionPlaceholder(targetItem, false, null);
763 if (itemOptionPlaceholder != null) {
764 placeholdersCopy.add(itemOptionPlaceholder);
767 String finalText = text;
768 // replace placeholder symbols
769 for (var placeholder : placeholdersCopy) {
770 var placeholderValue = placeholderValues.getOrDefault(placeholder.label, "");
771 if (!placeholderValue.isBlank()) {
772 placeholderValue = applyPOSTransformation(placeholderValue, placeholder);
774 finalText = finalText.replace(getPlaceholderSymbol(placeholder.label), placeholderValue);
776 // replace dynamic placeholder symbol
777 var dynamicValue = placeholderValues.getOrDefault(DYNAMIC_PLACEHOLDER, "");
778 if (!dynamicValue.isBlank()) {
779 finalText = finalText.replace(DYNAMIC_PLACEHOLDER_SYMBOL, dynamicValue);
784 protected ActionTemplateConfiguration[] getTypeActionConfigs(String itemType) {
785 File actionConfigsFile = Path.of(TYPE_ACTION_CONFIGS_FOLDER, itemType + ".json").toFile();
786 logger.debug("loading action templates configuration file {}", actionConfigsFile);
787 if (actionConfigsFile.exists() && !actionConfigsFile.isDirectory()) {
789 return ActionTemplateConfiguration.fromJSON(actionConfigsFile);
790 } catch (IOException e) {
791 logger.warn("unable to parse action templates configuration for type {}: {}", itemType, e.getMessage());
794 logger.debug("action templates configuration for type {} not available", itemType);
795 return new ActionTemplateConfiguration[] {};
798 private @Nullable Item getTargetItemByLabelTokens(String[] tokens) {
799 var label = getItemsByLabelTokensMap().entrySet().stream()
800 .filter(entry -> Arrays.equals(tokens, entry.getKey())).findFirst();
801 if (label.isEmpty()) {
804 return label.get().getValue();
807 private String[] getTargetItemTokens(String[] tokens, Span itemLabelSpan) {
808 return Arrays.copyOfRange(tokens, itemLabelSpan.getStart(), itemLabelSpan.getEnd());
811 private String[] replacePlaceholder(String text, String[] tokens, Span span, String placeholderName,
812 @Nullable List<NLPPlaceholderData> replacements, @Nullable Map<String[], String> valueByTokensMap) {
813 if (replacements != null) {
814 var spanTokens = Arrays.copyOfRange(tokens, span.getStart(), span.getEnd());
816 if (valueByTokensMap != null) {
817 var match = valueByTokensMap.entrySet().stream()
818 .filter(entry -> Arrays.equals(spanTokens, entry.getKey())).findFirst();
819 if (match.isPresent()) {
820 value = match.get().getValue();
822 value = getSpanTokens(tokens, span, text);
825 value = getSpanTokens(tokens, span, text);
827 replacements.add(new NLPPlaceholderData(placeholderName, value, span));
829 var spanStart = span.getStart();
830 try (Stream<String> dataStream = Stream.concat(
831 spanStart != 0 ? Arrays.stream(Arrays.copyOfRange(tokens, 0, spanStart)) : Stream.of(),
832 Arrays.stream(new String[] { getPlaceholderSymbol(placeholderName) }))) {
833 var spanEnd = span.getEnd();
834 if (spanEnd != tokens.length) {
835 return Stream.concat(dataStream, Arrays.stream(Arrays.copyOfRange(tokens, spanEnd, tokens.length)))
836 .toArray(String[]::new);
838 return dataStream.toArray(String[]::new);
842 private String getSpanTokens(String[] tokens, Span span, String original) {
843 return detokenize(Arrays.copyOfRange(tokens, span.getStart(), span.getEnd()), original);
846 private String detokenize(String[] tokens, String text) {
847 if (tokens.length == 1) {
850 if (config.detokenizeOptimization) {
851 // this is a dynamic regex to de-tokenize a part of the text based on the original text,
852 // this way we don't miss special characters between tokens.
853 var detokenizeRegex = String.join("[^a-bA-B0-9]?", tokens);
854 var match = Pattern.compile(detokenizeRegex).matcher(text);
856 return match.group();
858 logger.warn("Unable to detokenize using build-in optimization, consider reporting this case");
860 // Detokenize should be improved in the future, Detokenizer API seems to be a work in progress in OpenNLP
861 return String.join(" ", tokens);
864 private Tokenizer getTokenizer() {
867 var tokenModelFile = Path.of(NLP_FOLDER, "token.bin").toFile();
868 if (tokenModelFile.exists()) {
869 logger.debug("Tokenizing with model {}", tokenModelFile);
870 InputStream inputStream = new FileInputStream(tokenModelFile);
871 TokenizerModel model = new TokenizerModel(inputStream);
872 tokenizer = new TokenizerME(model);
874 if (config.useSimpleTokenizer) {
875 logger.debug("Using simple tokenizer");
876 tokenizer = SimpleTokenizer.INSTANCE;
878 logger.debug("Using white space tokenizer");
879 tokenizer = WhitespaceTokenizer.INSTANCE;
883 } catch (IOException e) {
884 logger.warn("IOException while loading tokenizer: {}", e.getMessage());
885 if (config.useSimpleTokenizer) {
886 logger.warn("Fallback to simple tokenizer");
887 return SimpleTokenizer.INSTANCE;
889 logger.warn("Fallback to white space tokenizer");
890 return WhitespaceTokenizer.INSTANCE;
895 private String[] tokenizeText(String text) {
896 return tokenizer.tokenize(text);
899 private Span[] nerWithFile(String[] tokens, String placeholderFileName) throws IOException {
900 File nerModelFile = Path.of(NER_FOLDER, placeholderFileName + ".bin").toFile();
901 File nerDictionaryFile = Path.of(NER_FOLDER, placeholderFileName + ".xml").toFile();
902 if (nerModelFile.exists()) {
903 return nerWithModel(tokens, nerModelFile);
904 } else if (nerDictionaryFile.exists()) {
905 return nerWithDictionary(tokens, nerDictionaryFile, getPlaceholderSymbol(placeholderFileName));
907 logger.debug("No model or dictionary found for '{}'", placeholderFileName);
908 throw new IOException("Unable to find model or dictionary with name: " + placeholderFileName);
912 private Span[] nerItemLabels(String[] tokens) {
913 return nerValues(tokens, getItemsByLabelTokensMap().keySet().toArray(String[][]::new), ITEM_LABEL_PLACEHOLDER,
917 private Span[] nerWithModel(String[] tokens, File nerModelFile) throws IOException {
918 logger.debug("applying NER with model {}", nerModelFile.getAbsolutePath());
919 TokenNameFinderModel model = new TokenNameFinderModel(nerModelFile);
920 var nameFinder = new NameFinderME(model);
921 return nameFinder.find(tokens);
924 private Span[] nerWithDictionary(String[] tokens, File nerDictFile, String type) throws IOException {
925 logger.debug("applying NER with dictionary {}", nerDictFile);
926 var dictionary = new opennlp.tools.dictionary.Dictionary(new FileInputStream(nerDictFile));
927 return nerWithDictionary(tokens, dictionary, type);
930 private Span[] nerValues(String[] tokens, String[][] valueTokens, String type) {
931 return nerValues(tokens, valueTokens, type, config.caseSensitive);
934 private Span[] nerValues(String[] tokens, String[][] valueTokens, String type, boolean caseSensitive) {
935 var runtimeDictionary = new Dictionary(caseSensitive);
936 Arrays.stream(valueTokens).map(StringList::new).forEach(runtimeDictionary::put);
937 return nerWithDictionary(tokens, runtimeDictionary, type);
940 private Span[] nerWithDictionary(String[] tokens, Dictionary dictionary, String type) {
941 var nameFinder = new DictionaryNameFinder(dictionary, type);
942 return nameFinder.find(tokens);
945 private String[] languagePOSTagging(String[] tokens) throws IOException {
946 var posTaggingModelFile = Path.of(NLP_FOLDER, "pos.bin").toFile();
947 if (posTaggingModelFile.exists()) {
948 logger.debug("applying POSTagging with model {}", posTaggingModelFile);
949 POSModel posModel = new POSModel(posTaggingModelFile);
950 POSTaggerME posTagger = new POSTaggerME(posModel);
951 return posTagger.tag(tokens);
953 logger.debug("POSTagging model not found {}, disabled", posTaggingModelFile);
954 return new String[] {};
958 private String[] languageLemmatize(String[] tokens, String[] tags) throws IOException {
959 if (tags.length == 0) {
960 logger.debug("Tags are required for lemmatization, disabled");
961 return new String[] {};
963 var lemmatizeModelFile = Path.of(NLP_FOLDER, "lemma.bin").toFile();
964 var lemmatizeDictionaryFile = Path.of(NLP_FOLDER, "lemma.txt").toFile();
965 Lemmatizer lemmatizer;
966 if (lemmatizeModelFile.exists()) {
967 logger.debug("applying lemmatize with model {}", lemmatizeModelFile);
968 LemmatizerModel model = new LemmatizerModel(lemmatizeModelFile);
969 lemmatizer = new LemmatizerME(model);
970 } else if (lemmatizeDictionaryFile.exists()) {
971 logger.debug("applying lemmatize with dictionary {}", lemmatizeDictionaryFile);
972 lemmatizer = new DictionaryLemmatizer(lemmatizeDictionaryFile);
974 logger.debug("Unable to find lemmatize dictionary or model, disabled");
975 return new String[] {};
977 return lemmatizer.lemmatize(tokens, tags);
980 private Map<String[], String> getStringsByTokensMap(String[] values) {
981 var map = new HashMap<String[], String>();
982 for (String value : values) {
983 map.put(tokenizeText(value), value);
988 private String applyPOSTransformation(String text, ActionTemplatePlaceholder placeholderConfig) throws IOException {
989 var singleWorldText = text.replaceAll("\\s", "__");
991 if (placeholderConfig.posFile != null) {
992 File posTaggingDictionary = Path.of(POS_FOLDER, placeholderConfig.posFile + ".xml").toFile();
993 File posTaggingModel = Path.of(POS_FOLDER, placeholderConfig.posFile + ".bin").toFile();
994 if (posTaggingModel.exists()) {
995 POSModel posModel = new POSModel(posTaggingModel);
996 var tags = new POSTaggerME(posModel).tag(new String[] { singleWorldText });
997 if (tags.length > 0 && !"O".equals(tags[0])) {
1000 } else if (posTaggingDictionary.exists()) {
1001 POSDictionary posDictionary = POSDictionary.create(new FileInputStream(posTaggingDictionary));
1002 var tags = posDictionary.getTags(singleWorldText);
1003 if (tags != null && tags.length > 0 && !"O".equals(tags[0])) {
1007 logger.warn("configured pos transformation file not found {}", placeholderConfig.posFile);
1009 } else if (placeholderConfig.posStaticValues != null) {
1010 var dictionary = new POSDictionary(config.caseSensitive);
1011 for (var entry : placeholderConfig.posStaticValues.entrySet()) {
1012 dictionary.put(entry.getKey(), entry.getValue());
1014 var tokenTags = dictionary.getTags(singleWorldText);
1015 if (tokenTags != null && tokenTags.length > 0) {
1019 // no transformation configured
1025 return tag.replace("__", " ");
1028 private Map<String[], Item> getItemsByLabelTokensMap() {
1029 return getItemsMaps().itemLabelByTokens;
1032 private Map<Item, ActionTemplateConfiguration[]> getItemsWithActionConfigs() {
1033 return getItemsMaps().itemsWithActionConfigs;
1036 private NLPItemMaps getItemsMaps() {
1037 var itemMaps = this.nlpItemMaps;
1038 if (itemMaps == null) {
1039 var itemByLabelTokens = new HashMap<String[], Item>();
1040 var itemsWithActionConfigs = new HashMap<Item, ActionTemplateConfiguration[]>();
1041 var labelList = new ArrayList<String>();
1042 for (Item item : itemRegistry.getAll()) {
1043 var alternativeNames = new ArrayList<String>();
1044 var label = item.getLabel();
1045 if (label != null) {
1046 alternativeNames.add(label);
1048 MetadataKey key = new MetadataKey("synonyms", item.getName());
1049 Metadata synonymsMetadata = metadataRegistry.get(key);
1050 if (synonymsMetadata != null) {
1051 String[] synonyms = synonymsMetadata.getValue().split(",");
1052 if (synonyms.length > 0) {
1053 alternativeNames.addAll(List.of(synonyms));
1056 if (!alternativeNames.isEmpty()) {
1057 for (var alternative : alternativeNames) {
1058 var lowerLabel = alternative.toLowerCase();
1059 if (labelList.contains(lowerLabel)) {
1060 logger.debug("Multiple items with label '{}', this is not supported, ignoring '{}'",
1061 lowerLabel, item.getName());
1064 labelList.add(lowerLabel);
1065 itemByLabelTokens.put(tokenizeText(lowerLabel), item);
1068 var metadata = metadataRegistry.get(new MetadataKey(SERVICE_ID, item.getName()));
1069 if (metadata != null) {
1071 itemsWithActionConfigs.put(item, ActionTemplateConfiguration.fromMetadata(metadata));
1072 } catch (IOException e) {
1073 logger.warn("Unable to parse template action configs for item '{}': {}", item.getName(),
1078 itemMaps = new NLPItemMaps(itemByLabelTokens, itemsWithActionConfigs);
1079 this.nlpItemMaps = itemMaps;
1084 private NLPTokenComparisonResult compareTokens(String[] tokens, String[] tokenTags, String[] tokensTemplate) {
1085 if (tokens.length == 0 || tokensTemplate.length == 0) {
1086 return NLPTokenComparisonResult.ZERO;
1089 int processedIndex = 0;
1090 // avoid use tags if not available for all tokens
1091 var tagsEnabled = tokenTags.length == tokens.length;
1092 for (int i = 0; i < tokens.length; i++) {
1093 String token = tokens[i];
1094 // Tag is used here to allow optional matching by language POS tag
1095 String tag = tagsEnabled ? tokenTags[i] : null;
1096 if (processedIndex == tokensTemplate.length) {
1097 return NLPTokenComparisonResult.ZERO;
1099 String tokenTemplate = tokensTemplate[processedIndex];
1100 var tokenAlternatives = splitString(tokenTemplate, "\\|");
1101 boolean isMatch = false;
1102 for (var tokenAlternative : tokenAlternatives) {
1103 if (DYNAMIC_PLACEHOLDER_SYMBOL.equals(tokenAlternative)) {
1104 if (tokenAlternatives.length > 1) {
1105 logger.warn("Providing the dynamic placeholder as an optional token is not allowed");
1106 return NLPTokenComparisonResult.ZERO;
1108 if (tokensTemplate.length == 1) {
1109 logger.warn("Providing the dynamic placeholder alone is not allowed");
1110 return NLPTokenComparisonResult.ZERO;
1112 if (processedIndex + 1 == tokensTemplate.length) {
1113 // the dynamic placeholder is the last value in the template token array, returning score
1114 // note that the dynamic placeholder does not count for score
1115 return new NLPTokenComparisonResult(score, new Span(i, tokensTemplate.length));
1117 // here we cut and reverse the arrays to run score backwards until the dynamic placeholder
1118 var unprocessedTokens = Arrays.copyOfRange(tokens, i, tokens.length);
1119 var unprocessedTags = tagsEnabled ? Arrays.copyOfRange(tokenTags, i, tokenTags.length)
1121 var unprocessedTokensTemplate = Arrays.copyOfRange(tokensTemplate, processedIndex,
1122 tokensTemplate.length);
1123 Collections.reverse(Arrays.asList(unprocessedTokens));
1124 Collections.reverse(Arrays.asList(unprocessedTags));
1125 Collections.reverse(Arrays.asList(unprocessedTokensTemplate));
1126 if (DYNAMIC_PLACEHOLDER_SYMBOL.equals(unprocessedTokens[0])) {
1127 // here dynamic placeholder should be at the end, but if it's also at the beginning we should
1129 logger.warn("Using multiple dynamic placeholders is not supported");
1130 return NLPTokenComparisonResult.ZERO;
1132 var partialScoreResult = compareTokens(unprocessedTokens, unprocessedTags,
1133 unprocessedTokensTemplate);
1134 if (NLPTokenComparisonResult.ZERO.equals(partialScoreResult)) {
1135 return NLPTokenComparisonResult.ZERO;
1137 var dynamicSpan = partialScoreResult.dynamicSpan;
1138 if (dynamicSpan == null) {
1140 "dynamic span missed, this should never happen, please open an issue; aborting");
1141 return NLPTokenComparisonResult.ZERO;
1143 return new NLPTokenComparisonResult(score + partialScoreResult.score,
1144 new Span(i, tokens.length - (dynamicSpan.getStart())));
1147 if (tokenAlternative.equals(token)) {
1155 } else if (tag != null && optionalLanguageTags.contains(tag)) {
1156 logger.debug("part '{}' tagged as '{}' skipped", token, tag);
1158 return NLPTokenComparisonResult.ZERO;
1161 return new NLPTokenComparisonResult(score, null);
1164 private String[] splitString(String template, String regex) {
1165 return Arrays.stream(template.split(regex)).map(String::trim).toArray(String[]::new);
1168 private void invalidate() {
1169 logger.debug("Invalidate cached item data");
1173 private void reloadConfigs() {
1174 optionalLanguageTags = Arrays.stream(this.config.optionalLanguageTags.split(",")).filter(i -> !i.isEmpty())
1175 .collect(Collectors.toList());
1176 tokenizer = getTokenizer();
1179 private static class NLPInfo {
1180 public final String[] tokens;
1181 public final String[] lemmas;
1182 public final String[] tags;
1184 public NLPInfo(String[] tokens, String[] lemmas, String[] tags) {
1185 this.tokens = tokens;
1186 this.lemmas = lemmas;
1191 private static void createFolder(Logger logger, String nlpFolder) {
1192 File directory = new File(nlpFolder);
1193 if (!directory.exists()) {
1194 if (directory.mkdir()) {
1195 logger.debug("dir created {}", nlpFolder);
1200 public static String getPlaceholderSymbol(String name) {
1201 return "$" + name.replaceAll("\\s", "_");
1204 private static class NLPInterpretationResult {
1205 public final Item targetItem;
1206 public final ActionTemplateConfiguration actionConfig;
1207 public final Map<String, String> placeholderValues;
1209 public NLPInterpretationResult(Item targetItem, ActionTemplateConfiguration actionConfig,
1210 Map<String, String> placeholderValues) {
1211 this.targetItem = targetItem;
1212 this.actionConfig = actionConfig;
1213 this.placeholderValues = placeholderValues;
1216 public static NLPInterpretationResult from(Item item, ActionTemplateConfiguration actionConfig,
1217 List<NLPPlaceholderData> placeholderValues) {
1218 return new NLPInterpretationResult(item, actionConfig, placeholderValues.stream()
1219 .collect(Collectors.toMap(i -> i.placeholderName, i -> i.placeholderValue)));
1223 public static class NLPPlaceholderData {
1224 private final String placeholderName;
1225 private final String placeholderValue;
1226 private final Span placeholderSpan;
1228 private NLPPlaceholderData(String placeholderName, String placeholderValue, Span placeholderSpan) {
1229 this.placeholderName = placeholderName;
1230 this.placeholderValue = placeholderValue;
1231 this.placeholderSpan = placeholderSpan;
1235 private static class NLPTokenComparisonResult {
1236 public static final NLPTokenComparisonResult ZERO = new NLPTokenComparisonResult(0, null);
1237 public final int score;
1238 public final @Nullable Span dynamicSpan;
1240 private NLPTokenComparisonResult(int score, @Nullable Span dynamicSpan) {
1242 this.dynamicSpan = dynamicSpan;
1246 private static class NLPItemMaps {
1247 private final Map<String[], Item> itemLabelByTokens;
1248 private final Map<Item, ActionTemplateConfiguration[]> itemsWithActionConfigs;
1250 private NLPItemMaps(Map<String[], Item> itemLabelByTokens,
1251 Map<Item, ActionTemplateConfiguration[]> itemsWithActionConfigs) {
1252 this.itemLabelByTokens = itemLabelByTokens;
1253 this.itemsWithActionConfigs = itemsWithActionConfigs;