Skip to content
This repository has been archived by the owner on May 23, 2019. It is now read-only.

Commit

Permalink
Item resolver implementation with ESH semantics (#24)
Browse files Browse the repository at this point in the history
BREAKING: Requires an openHAB distribution including openhab/openhab-core#415 to work!
Related to eclipse-archived/smarthome#6288. Requires compatible tags.

Extract name samples from tags, item labels & synonyms (comma-separated strings in the "synonyms" metadata namespace)

Other misc changes:

* Fix location dropdown counters in card deck
* Fix invisible send button in chat text field
* Better title & subtitle in generated chart cards
* Expand matched groups automatically
* Fail most skills if no entities found

Signed-off-by: Yannick Schaus <habpanel@schaus.net>
  • Loading branch information
ghys authored Oct 14, 2018
1 parent ebb4260 commit 4f029b4
Show file tree
Hide file tree
Showing 23 changed files with 448 additions and 110 deletions.
2 changes: 2 additions & 0 deletions META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ Import-Package: com.google.common.collect,
org.eclipse.smarthome.core.library,
org.eclipse.smarthome.core.library.types,
org.eclipse.smarthome.core.persistence,
org.eclipse.smarthome.core.semantics,
org.eclipse.smarthome.core.semantics.model,
org.eclipse.smarthome.core.storage,
org.eclipse.smarthome.core.transform,
org.eclipse.smarthome.core.types,
Expand Down
10 changes: 7 additions & 3 deletions src/main/java/org/openhab/ui/habot/card/CardBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ public Card buildChartCard(Intent intent, Collection<Item> matchedItems, String
if (matchingNonGroupItems.get().count() == 1) {
Item item = matchingNonGroupItems.get().findFirst().get();
card.setTitle(item.getLabel());
card.setSubtitle(item.getName());
card.setSubtitle(period + " - " + item.getName());
} else {
GroupItem commonGroup = getMatchingGroup(matchedItems);
if (commonGroup != null) {
Expand All @@ -294,9 +294,10 @@ public Card buildChartCard(Intent intent, Collection<Item> matchedItems, String
card.addComponent("right", singleItemComponent);
}
} else {
card.setTitle(String.join(", ", intent.getEntities().values()));
card.setTitle(intent.getEntities().entrySet().stream().filter(e -> !e.getKey().equals("period"))
.map(e -> e.getValue()).collect(Collectors.joining(", ")));
}
card.setSubtitle(matchingNonGroupItems.get().count() + " items"); // TODO: i18n
card.setSubtitle(period + " - " + matchingNonGroupItems.get().count() + " items"); // TODO: i18n
}

Component chart = new Component("HbChartImage");
Expand Down Expand Up @@ -338,6 +339,9 @@ private String formatState(Item item, State state) throws TransformationExceptio
String transformedState = TransformationHelper.transform(
FrameworkUtil.getBundle(CardBuilder.class).getBundleContext(),
stateDescription.getPattern(), state.toString());
if (transformedState == null) {
return state.toString();
}
if (transformedState.equals(state.toString())) {
return state.format(stateDescription.getPattern());
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.smarthome.core.items.GroupItem;
import org.eclipse.smarthome.core.items.Item;
import org.eclipse.smarthome.core.items.ItemRegistry;
import org.openhab.ui.habot.nlp.internal.AnswerFormatter;
import org.openhab.ui.habot.nlp.internal.ItemNamedAttributesResolver;
import org.openhab.ui.habot.nlp.internal.NamedAttributesItemResolver;

/**
* An abstract implmentation of a {@link Skill} with helper methods to find items matching an {@link Intent}
Expand All @@ -28,24 +29,35 @@
public abstract class AbstractItemIntentInterpreter implements Skill {

protected ItemRegistry itemRegistry;
protected ItemNamedAttributesResolver itemNamedAttributesResolver;
protected ItemResolver itemResolver;
protected AnswerFormatter answerFormatter;

/**
* Returns the items matching the entities in the intent.
* It delegates this task to the {@link ItemNamedAttributesResolver} to find named attributes
* It delegates this task to the {@link NamedAttributesItemResolver} to find named attributes
* matching the entities.
*
* The resulting items should match the object AND the location if both are provided.
*
* @param intent the {@link Intent} containing the entities to match to items' tags.
* @return the set of matching items
* @throws UnsupportedLanguageException
*/
protected Set<Item> findItems(Intent intent) {
String object = intent.getEntities().get("object");
String location = intent.getEntities().get("location");

return this.itemNamedAttributesResolver.getMatchingItems(object, location).collect(Collectors.toSet());
Set<Item> items = this.itemResolver.getMatchingItems(object, location).collect(Collectors.toSet());

// expand group items
for (Item item : items) {
if (item instanceof GroupItem) {
GroupItem gItem = (GroupItem) item;
items.addAll(gItem.getMembers());
}
}

return items;
}

@Override
Expand Down
30 changes: 26 additions & 4 deletions src/main/java/org/openhab/ui/habot/nlp/ItemNamedAttribute.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,43 @@
* @author Yannick Schaus
*/
public class ItemNamedAttribute {
public enum AttributeType {
OBJECT,
LOCATION
}

public enum AttributeSource {
LABEL,
CATEGORY,
TAG,
METADATA
}

public ItemNamedAttribute(String type, String value, boolean inherited, AttributeSource source) {
public ItemNamedAttribute(AttributeType type, String value, boolean inherited, AttributeSource source) {
super();
this.type = type;
this.value = value;
this.inherited = inherited;
this.source = source;
}

String type;
public ItemNamedAttribute(String type, String value, boolean inherited, AttributeSource source) {
super();
this.type = (type == "location") ? AttributeType.LOCATION : AttributeType.OBJECT;
this.value = value;
this.inherited = inherited;
this.source = source;
}

public ItemNamedAttribute(String type, String value, AttributeSource source) {
super();
this.type = (type == "location") ? AttributeType.LOCATION : AttributeType.OBJECT;
this.value = value;
this.inherited = false;
this.source = source;
}

AttributeType type;
String value;
boolean inherited;
AttributeSource source;
Expand All @@ -40,7 +62,7 @@ public ItemNamedAttribute(String type, String value, boolean inherited, Attribut
*
* @return the type - "object" or "location"
*/
public String getType() {
public AttributeType getType() {
return type;
}

Expand All @@ -49,7 +71,7 @@ public String getType() {
*
* @param type the type
*/
public void setType(String type) {
public void setType(AttributeType type) {
this.type = type;
}

Expand Down
56 changes: 56 additions & 0 deletions src/main/java/org/openhab/ui/habot/nlp/ItemResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2018 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.ui.habot.nlp;

import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.eclipse.smarthome.core.items.Item;
import org.openhab.ui.habot.nlp.internal.IntentTrainer;

import opennlp.tools.namefind.NameSample;

/**
* This interface is used to find items matching entities extracted from the
* user natural language query: object - "what" and location - "where". It also acts as a supplemental source of @link
* {@link NameSample}s for the {@link IntentTrainer}
*
* @author Yannick Schaus
*/
public interface ItemResolver {

/**
* Sets the current locale.
* The ItemResolver will receive object and location entities in that locale.
*
* @param locale
*/
void setLocale(Locale locale) throws UnsupportedLanguageException;

/**
* Resolves items matching the provided object and/or location extracted from user's query using named-entity
* recognition in the current locale.
* If a non-null object and a non-null location are provided,
* items shall match both.
*
* @param object the object extracted from the intent (or null)
* @param location the location extracted from the intent (or null)
* @return a stream of matching items (groups included)
*/
Stream<Item> getMatchingItems(String object, String location);

/**
* Gets all named attributes for all items
*
* @return a map of the {@link ItemNamedAttribute}s by item
*/
Map<Item, Set<ItemNamedAttribute>> getAllItemNamedAttributes() throws UnsupportedLanguageException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
import org.eclipse.smarthome.core.items.MetadataRegistry;
import org.openhab.ui.habot.nlp.ItemNamedAttribute;
import org.openhab.ui.habot.nlp.ItemNamedAttribute.AttributeSource;
import org.openhab.ui.habot.nlp.ItemResolver;
import org.openhab.ui.habot.nlp.UnsupportedLanguageException;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -44,10 +44,10 @@
*
* @author Yannick Schaus
*/
@Component(service = ItemNamedAttributesResolver.class, immediate = true)
public class ItemNamedAttributesResolver {
// @Component(service = ItemResolver.class, immediate = true)
public class NamedAttributesItemResolver implements ItemResolver {

private final Logger logger = LoggerFactory.getLogger(ItemNamedAttributesResolver.class);
private final Logger logger = LoggerFactory.getLogger(NamedAttributesItemResolver.class);

private static final Set<String> LOCATION_CATEGORIES = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList("cellar", "livingroom", "kitchen", "bedroom", "bath", "toilet",
Expand All @@ -60,13 +60,12 @@ public class ItemNamedAttributesResolver {
private Locale currentLocale = null;
ResourceBundle tagAttributes;

/**
* Sets the current locale.
* Attributes derived from semantic tags will use this locale.
* This will invalidate the attribute cache.
/*
* (non-Javadoc)
*
* @param locale
* @see org.openhab.ui.habot.nlp.internal.ItemResolver#setLocale(java.util.Locale)
*/
@Override
public void setLocale(Locale locale) {
if (!locale.equals(currentLocale)) {
this.currentLocale = locale;
Expand All @@ -78,12 +77,12 @@ public void setLocale(Locale locale) {
}
}

/**
* Returns a map of all named attributes by item.
/*
* (non-Javadoc)
*
* @return attributes mapped by item
* @throws UnsupportedLanguageException
* @see org.openhab.ui.habot.nlp.internal.ItemResolver#getAllItemNamedAttributes()
*/
@Override
public Map<Item, Set<ItemNamedAttribute>> getAllItemNamedAttributes() throws UnsupportedLanguageException {
if (currentLocale == null) {
throw new UnsupportedLanguageException(currentLocale);
Expand Down Expand Up @@ -115,14 +114,12 @@ public Set<ItemNamedAttribute> getItemNamedAttributes(Item item) throws Unsuppor
return itemAttributes.get(item);
}

/**
* Returns items having attributes matching the provided object and locations.
* Both have to match if provided.
/*
* (non-Javadoc)
*
* @param object the object to find in the attributes
* @param location the location to find in the attribute
* @return a stream of matching items (groups included)
* @see org.openhab.ui.habot.nlp.internal.ItemResolver#getMatchingItems(java.lang.String, java.lang.String)
*/
@Override
public Stream<Item> getMatchingItems(String object, String location) {
return itemAttributes.entrySet().stream().filter(entry -> {
boolean objectMatch = false;
Expand Down Expand Up @@ -170,12 +167,13 @@ private void updateItemNamedAttributes() {
}
}
} else {
if (item.getCategory() != null) {
String category = item.getCategory();
if (category != null) {
if (metadata != null && metadata.getConfiguration().containsKey("useCategory")
&& metadata.getConfiguration().get("useCategory").equals(false)) {
logger.info("Ignoring category for item {}", item.getName());
} else {
String category = item.getCategory().toLowerCase();
category = category.toLowerCase();
String categoryNamedAttributes;
try {
categoryNamedAttributes = this.tagAttributes.getString(category);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import org.openhab.ui.habot.nlp.Intent;
import org.openhab.ui.habot.nlp.IntentInterpretation;
import org.openhab.ui.habot.nlp.ItemNamedAttribute;
import org.openhab.ui.habot.nlp.ItemNamedAttribute.AttributeType;
import org.openhab.ui.habot.nlp.ItemResolver;
import org.openhab.ui.habot.nlp.Skill;
import org.openhab.ui.habot.nlp.UnsupportedLanguageException;
import org.osgi.framework.BundleContext;
Expand Down Expand Up @@ -61,7 +63,7 @@ public class OpenNLPInterpreter implements HumanLanguageInterpreter {
private String tokenizerId = null;

private ItemRegistry itemRegistry;
private ItemNamedAttributesResolver itemNamedAttributesResolver;
private ItemResolver itemResolver;
private EventPublisher eventPublisher;

private HashMap<String, Skill> skills = new HashMap<String, Skill>();
Expand Down Expand Up @@ -103,15 +105,20 @@ public String interpret(Locale locale, String text) throws InterpretationExcepti
return reply.getAnswer();
}

private InputStream getNameSamplesFromItems(Locale locale) throws UnsupportedLanguageException {
/**
* Get an {@link InputStream} of additional name samples to feed to
* the {@link IntentTrainer} to improve the recognition.
*
* @return an OpenNLP compatible input stream with the tagged name samples on separate lines
*/
protected InputStream getNameSamples() throws UnsupportedLanguageException {
StringBuilder nameSamplesDoc = new StringBuilder();
itemNamedAttributesResolver.setLocale(locale);
Map<Item, Set<ItemNamedAttribute>> itemAttributes = itemNamedAttributesResolver.getAllItemNamedAttributes();
Map<Item, Set<ItemNamedAttribute>> itemAttributes = itemResolver.getAllItemNamedAttributes();

Stream<ItemNamedAttribute> attributes = itemAttributes.values().stream().flatMap(a -> a.stream());

attributes.forEach(attribute -> {
if (attribute.getType() == "location") {
if (attribute.getType() == AttributeType.LOCATION) {
nameSamplesDoc.append(String.format("<START:location> %s <END>%n", attribute.getValue()));
} else {
nameSamplesDoc.append(String.format("<START:object> %s <END>%n", attribute.getValue()));
Expand All @@ -132,6 +139,7 @@ private InputStream getNameSamplesFromItems(Locale locale) throws UnsupportedLan
public ChatReply reply(Locale locale, String text) throws InterpretationException {
if (!locale.equals(currentLocale) || intentTrainer == null) {
try {
itemResolver.setLocale(locale);
intentTrainer = new IntentTrainer(locale.getLanguage(),
skills.values().stream().sorted(new Comparator<Skill>() {

Expand All @@ -146,7 +154,7 @@ public int compare(Skill o1, Skill o2) {
return o1.getIntentId().compareTo(o2.getIntentId());
}

}).collect(Collectors.toList()), getNameSamplesFromItems(locale), this.tokenizerId);
}).collect(Collectors.toList()), getNameSamples(), this.tokenizerId);
currentLocale = locale;
} catch (Exception e) {
InterpretationException fe = new InterpretationException(
Expand All @@ -164,11 +172,11 @@ public int compare(Skill o1, Skill o2) {
// it a "get-status" intent with this attribute as the corresponding entity.
// This allows the user to query a named attribute quickly by simply stating it - and avoid a
// misinterpretation by the categorizer.
if (this.itemNamedAttributesResolver.getMatchingItems(text, null).findAny().isPresent()) {
if (this.itemResolver.getMatchingItems(text, null).findAny().isPresent()) {
intent = new Intent("get-status");
intent.setEntities(new HashMap<String, String>());
intent.getEntities().put("object", text.toLowerCase());
} else if (this.itemNamedAttributesResolver.getMatchingItems(null, text).findAny().isPresent()) {
} else if (this.itemResolver.getMatchingItems(null, text).findAny().isPresent()) {
intent = new Intent("get-status");
intent.setEntities(new HashMap<String, String>());
intent.getEntities().put("location", text.toLowerCase());
Expand Down Expand Up @@ -230,12 +238,12 @@ protected void unsetItemRegistry(ItemRegistry itemRegistry) {
}

@Reference
protected void setItemNamedAttributesResolver(ItemNamedAttributesResolver itemNamedAttributesResolver) {
this.itemNamedAttributesResolver = itemNamedAttributesResolver;
protected void setItemResolver(ItemResolver itemResolver) {
this.itemResolver = itemResolver;
}

protected void unsetItemNamedAttributesResolver(ItemNamedAttributesResolver itemNamedAttributesResolver) {
this.itemNamedAttributesResolver = null;
protected void unsetItemResolver(ItemResolver itemResolver) {
this.itemResolver = null;
}

@Reference
Expand Down
Loading

0 comments on commit 4f029b4

Please sign in to comment.