/* * Seldon -- open source prediction engine * ======================================= * * Copyright 2011-2015 Seldon Technologies Ltd and Rummble Ltd (http://www.seldon.io/) * * ******************************************************************************************** * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * 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 io.seldon.recommendation.explanation; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; import io.seldon.api.state.ClientConfigHandler; import io.seldon.api.state.ClientConfigUpdateListener; import io.seldon.memcache.DogpileHandler; import io.seldon.memcache.ExceptionSwallowingMemcachedClient; import io.seldon.memcache.MemCacheKeys; import io.seldon.memcache.UpdateRetriever; @Component public class ExplanationPeer implements ClientConfigUpdateListener { private static Logger logger = Logger.getLogger(ExplanationPeer.class.getName()); public static class RecommendationExplanationConfig { public boolean cache_enabled = false; public String default_locale = "us-en"; public boolean explanations_enabled = false; @Override public String toString() { return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE); } } private final static String RECOMMENDATION_EXPLANATION_MAPPING_KEY = "recommendation_explanation"; private ObjectMapper objMapper = new ObjectMapper(); private ExplanationProvider defaultExplanationProvider; final static int EXPLANATION_CACHE_TIME_SECS = 3600; final private ExceptionSwallowingMemcachedClient memcacheClient; private Map<String, RecommendationExplanationConfig> client_recommendation_explanation_configs = new ConcurrentHashMap<>(); private final static String DEFAULT_EXPLANATION = ""; // use this incase of a null explanation @Autowired public ExplanationPeer(ExceptionSwallowingMemcachedClient memcacheClient, ClientConfigHandler configHandler) { this.memcacheClient = memcacheClient; if (configHandler != null) { configHandler.addListener(this); } logger.info("initialized"); } public boolean isExplanationNeededForClient(String clientName) { boolean retVal = false; RecommendationExplanationConfig recommendationExplanationConfig = client_recommendation_explanation_configs.get(clientName); if ((recommendationExplanationConfig != null) && (recommendationExplanationConfig.explanations_enabled)) { retVal = true; } return retVal; } public String explainRecommendationResult(final String clientName, String recommenderIn, String localeIn) { final String recommender = normalizeRecommender(recommenderIn); RecommendationExplanationConfig recommendationExplanationConfig = client_recommendation_explanation_configs.get(clientName); if ((recommendationExplanationConfig == null) || (!recommendationExplanationConfig.explanations_enabled)) { return "Explanation not enbaled for client"; } final String locale; if (localeIn == null) { locale = recommendationExplanationConfig.default_locale; logger.debug(String.format("locale is null using defaults, locale[%s]", locale)); } else { locale = localeIn; } String explanation; if (defaultExplanationProvider != null) { explanation = defaultExplanationProvider.getExplanation(recommender, locale); } else { ExplanationProvider explanationProvider; if (recommendationExplanationConfig.cache_enabled) { String memKey = MemCacheKeys.getExplanationsKey(clientName, recommender, locale); explanation = (String) memcacheClient.get(memKey); logger.debug(String.format("memKey[%s], recommendationExplanation[%s]", memKey, explanation)); String newRes = null; try { newRes = DogpileHandler.get().retrieveUpdateIfRequired(memKey, explanation, new UpdateRetriever<String>() { @Override public String retrieve() throws Exception { SqlExplanationProvider sqlExplanationProvider = new SqlExplanationProvider(clientName); return sqlExplanationProvider.getExplanation(recommender, locale); } }, EXPLANATION_CACHE_TIME_SECS); } catch (Exception e) { logger.warn("Error when retrieving static recommendations in dogpile handler ", e); } if (newRes != null) { memcacheClient.set(memKey, EXPLANATION_CACHE_TIME_SECS, newRes); explanation = newRes; } } else { explanationProvider = new SqlExplanationProvider(clientName); explanation = explanationProvider.getExplanation(recommender, locale); } } if (explanation == null) { explanation = DEFAULT_EXPLANATION; } logger.debug(String.format("explaining [%s] as [%s]", recommender, explanation)); return explanation; } public void setExplanationProvider(ExplanationProvider explanationProvider) { defaultExplanationProvider = explanationProvider; } private RecommendationExplanationConfig getRecommendationExplanationConfigFromJson(String json) throws IOException { RecommendationExplanationConfig recommendationExplanationConfig = null; recommendationExplanationConfig = objMapper.readValue(json, RecommendationExplanationConfig.class); return recommendationExplanationConfig; } public void updateRecommendationExplanationConfig(String client, String json) { try { RecommendationExplanationConfig recommendationExplanationConfig = getRecommendationExplanationConfigFromJson(json); client_recommendation_explanation_configs.put(client, recommendationExplanationConfig); logger.info(String.format("Updated recommendation explanation config for client[%s] value[%s]", client, recommendationExplanationConfig)); } catch (Exception e) { logger.error(String.format("Failed to update recommendation explanation config using json[%s]", json), e); } } public void removeRecommendationExplanationConfig(String client) { client_recommendation_explanation_configs.remove(client); logger.info(String.format("Removed recommendation explanation config for client[%s]", client)); } @Override public void configUpdated(String client, String configKey, String configValue) { if (configKey.equals(RECOMMENDATION_EXPLANATION_MAPPING_KEY)) { logger.info("Received new recommendation explanation config for " + client + ": " + configValue); updateRecommendationExplanationConfig(client, configValue); } } @Override public void configRemoved(String client, String configKey) { if (configKey.equals(RECOMMENDATION_EXPLANATION_MAPPING_KEY)) { removeRecommendationExplanationConfig(client); } } public static String normalizeRecommender(String recommender) { String retVal = recommender; int n = recommender.indexOf(':'); if (n >= 0) { retVal = retVal.substring(0, n); } return retVal; } }