All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nlpcraft.examples.weather.WeatherModel Maven / Gradle / Ivy

There is a newer version: 0.8.2
Show newest version
/*
 * “Commons Clause” License, https://commonsclause.com/
 *
 * The Software is provided to you by the Licensor under the License,
 * as defined below, subject to the following condition.
 *
 * Without limiting other conditions in the License, the grant of rights
 * under the License will not include, and the License does not grant to
 * you, the right to Sell the Software.
 *
 * For purposes of the foregoing, “Sell” means practicing any or all of
 * the rights granted to you under the License to provide to third parties,
 * for a fee or other consideration (including without limitation fees for
 * hosting or consulting/support services related to the Software), a
 * product or service whose value derives, entirely or substantially, from
 * the functionality of the Software. Any license notice or attribution
 * required by the License must also include this Commons Clause License
 * Condition notice.
 *
 * Software:    NLPCraft
 * License:     Apache 2.0, https://www.apache.org/licenses/LICENSE-2.0
 * Licensor:    Copyright (C) 2018 DataLingvo, Inc. https://www.datalingvo.com
 *
 *     _   ____      ______           ______
 *    / | / / /___  / ____/________ _/ __/ /_
 *   /  |/ / / __ \/ /   / ___/ __ `/ /_/ __/
 *  / /|  / / /_/ / /___/ /  / /_/ / __/ /_
 * /_/ |_/_/ .___/\____/_/   \__,_/_/  \__/
 *        /_/
 */

package org.nlpcraft.examples.weather;

import com.google.gson.*;
import org.apache.commons.lang3.tuple.*;
import org.nlpcraft.examples.misc.apixu.*;
import org.nlpcraft.examples.misc.apixu.beans.*;
import org.nlpcraft.examples.misc.geo.keycdn.*;
import org.nlpcraft.examples.misc.geo.keycdn.beans.*;
import org.nlpcraft.model.*;
import org.nlpcraft.model.builder.*;
import org.nlpcraft.model.intent.*;
import org.nlpcraft.model.intent.NCIntentSolver.*;
import org.nlpcraft.model.utils.*;
import java.time.*;
import java.util.*;
import java.util.function.*;

import static org.nlpcraft.model.utils.NCTokenUtils.*;

/**
 * Weather example model provider.
 * 

* This is relatively complete weather service with JSON output and a non-trivial * intent matching logic. It uses https://www.apixu.com REST service for the actual * weather information. *

* Note that this class is mostly identical to {@link WeatherModel} * except for the output formatting. This implementation uses JSON. Note also that it also returns intent ID * together with execution result which can be used in testing. */ @SuppressWarnings("Duplicates") public class WeatherModel extends NCModelProviderAdapter { // It is demo token and its usage has some restrictions (history data contains one day only, etc). // Please register your own account at https://www.apixu.com/pricing.aspx and // replace this demo token with your own. private final ApixuWeatherService srv = new ApixuWeatherService("3f9b7de2d3894eb6b27150825171909"); // Geo manager. private final GeoManager geoMrg = new GeoManager(); private static final Gson gson = new Gson(); // Maximum free words left before rejection. private static final int MAX_FREE_WORDS = 4; // Keywords for 'local' weather. private static final Set LOCAL_WORDS = new HashSet<>(Arrays.asList("my", "local", "hometown")); /** * A shortcut to convert millis to the local date object. * * @param ms Millis to convert. * @return Local date object. */ private LocalDate toLocalDate(long ms) { return Instant.ofEpochMilli(ms).atZone(ZoneId.systemDefault()).toLocalDate(); } /** * Makes JSON result for the given date range weather. * * @param res Weather holder for the range of dates. * @param intentId Intent ID. * @return Query result. */ private NCQueryResult makeRangeResult(RangeResponse res, String intentId) { Location loc = res.getLocation(); if (loc == null) throw new NCRejection("Weather service doesn't recognize this location."); return NCQueryResult.json(gson.toJson(new WeatherResultWrapper<>(intentId, res))); } /** * Makes JSON result for a single date. * * @param res Weather holder for a single date. * @param intentId Intent ID. * @return Query result. */ private NCQueryResult makeCurrentResult(CurrentResponse res, String intentId) { Location loc = res.getLocation(); Current cur = res.getCurrent(); if (loc == null) throw new NCRejection("Weather service doesn't recognize this location."); if (cur == null) throw new NCRejection("Weather service doesn't support this location."); return NCQueryResult.json(gson.toJson(new WeatherResultWrapper<>(intentId, res))); } /** * Extracts date range from given solver context. * * @param ctx Solver context. * @return Pair of dates or {@code null} if not found. */ private Pair prepDate(NCIntentSolverContext ctx) { List toks = ctx.getIntentTokens().get(1); if (toks.size() > 1) throw new NCRejection("Only one date is supported."); if (toks.size() == 1) { NCToken tok = toks.get(0); return Pair.of(toLocalDate(getDateFrom(tok)), toLocalDate(getDateTo(tok))); } // No date found - return 'null'. return null; } /** * Extracts geo location (city) from given solver context that is suitable for APIXU service. * * @param ctx Solver context. * @return Geo location. */ private String prepGeo(NCIntentSolverContext ctx) throws NCRejection { List geoToks = ctx.getIntentTokens().get(2); // Can be empty... List allToks = ctx.getVariant().getTokens(); Optional geoOpt = geoMrg.get(ctx.getQueryContext().getSentence()); if (!geoOpt.isPresent()) throw new NCRejection("City cannot be determined."); GeoDataBean geo = geoOpt.get(); // Common lambda for getting current user city. Supplier curCityFn = () -> { // Try current user location. // APIXU weather service understands this format too. return geo.getLatitude() + "," + geo.getLongitude(); }; // Manually process request for local weather. We need to separate between 'local Moscow weather' // and 'local weather' which are different. Basically, if there is word 'local/my/hometown' in the user // input and there is no city in the current sentence - this is a request for the weather at user's // current location, i.e. we should implicitly assume user's location and clear conversion context. // In all other cases - we take location from either current sentence or conversation STM. // NOTE: we don't do this separation on intent level as it is easier to do it here instead of // creating more intents with almost identical callbacks. boolean hasLocalWord = allToks.stream().anyMatch(t -> LOCAL_WORDS.contains(getNormalizedText(t))); if (hasLocalWord && geoToks.isEmpty()) { // Because we implicitly assume user's current city at this point we need to clear // 'nlp:geo' tokens from conversation context since they would no longer be valid. ctx.getQueryContext().getConversationContext().clear(NCTokenUtils::isGeo); // Return user current city. return curCityFn.get(); } else return geoToks.size() == 1 ? getGeoCity(geoToks.get(0)) : curCityFn.get(); } /** * Makes query result for given date range. * * @param ctx Token solver context. * @param from Default from date. * @param to Default to date. * @return Query result. */ private NCQueryResult onRangeMatch(NCIntentSolverContext ctx, LocalDate from, LocalDate to) { Pair date = prepDate(ctx); if (date == null) // If we don't have the date in the sentence or conversation STM - use default range. date = Pair.of(from, to); String geo = prepGeo(ctx); try { return makeRangeResult(srv.getWeather(geo, date), ctx.getIntentId()); } catch (ApixuPeriodException e) { throw new NCRejection(e.getLocalizedMessage()); } } /** * Strict check for an exact match (i.e. no dangling unused system or user defined tokens) and * maximum number of free words left unmatched. In both cases user input will be rejected. * * @param ctx Solver context. */ private void checkMatch(NCIntentSolverContext ctx) { // Reject if intent match is not exact ("dangling" tokens remain) or too many free words left unmatched. if (!ctx.isExactMatch() || ctx.getVariant().getTokens().stream().filter(NCTokenUtils::isFreeWord).count() > MAX_FREE_WORDS) throw new NCRejection("Please simplify your request."); } /** * Callback on forecast intent match. * * @param ctx Token solver context. * @return Query result. */ private NCQueryResult onForecastMatch(NCIntentSolverContext ctx) { checkMatch(ctx); try { // Look 5 days ahead by default. return onRangeMatch(ctx, LocalDate.now(), LocalDate.now().plusDays(5)); } catch (NCRejection e) { throw e; } catch (Exception e) { throw new NCRejection("Weather provider error.", e); } } /** * Callback on history intent match. * * @param ctx Token solver context. * @return Query result. */ private NCQueryResult onHistoryMatch(NCIntentSolverContext ctx) { checkMatch(ctx); try { // Look 5 days back by default. return onRangeMatch(ctx, LocalDate.now().minusDays(5), LocalDate.now()); } catch (NCRejection e) { throw e; } catch (Exception e) { throw new NCRejection("Weather provider error.", e); } } /** * Callback on current date intent match. * * @param ctx Token solver context. * @return Query result. */ private NCQueryResult onCurrentMatch(NCIntentSolverContext ctx) { checkMatch(ctx); try { Pair date = prepDate(ctx); String geo = prepGeo(ctx); return date != null ? makeRangeResult(srv.getWeather(geo, date), ctx.getIntentId()) : makeCurrentResult(srv.getCurrentWeather(geo), ctx.getIntentId()); } catch (ApixuPeriodException e) { throw new NCRejection(e.getLocalizedMessage()); } catch (NCRejection e) { throw e; } catch (Exception e) { throw new NCRejection("Weather provider error.", e); } } /** * Shortcut for creating a conversational intent with given weather token. * * @param weatherTokId Specific weather token ID. * @return Newly created intent. */ private INTENT mkIntent(String id, String weatherTokId) { return new CONV_INTENT( id, new TERM("id == " + weatherTokId, 1, 1), // Index 0: mandatory 'weather' token. new TERM("id == nlp:date", 0, 1), // Index 1: optional date. new TERM( // Index 2: optional city. new AND("id == nlp:geo", "~GEO_KIND == CITY"), 0, 1 ) ); } /** * Initializes model provider. */ public WeatherModel() { // If no intent is matched respond with some helpful message... NCIntentSolver solver = new NCIntentSolver("solver", () -> { throw new NCRejection("Weather request is ambiguous."); }); // Match exactly one of weather tokens and optional 'nlp:geo' and 'nlp:date' tokens. solver.addIntent(mkIntent("hist|date?|city?", "wt:hist"), this::onHistoryMatch); solver.addIntent(mkIntent("fcast|date?|city?", "wt:fcast"), this::onForecastMatch); solver.addIntent(mkIntent("curr|date?|city?", "wt:curr"), this::onCurrentMatch); setup(NCModelBuilder. newJsonModel(WeatherModel.class. getClassLoader(). getResourceAsStream("org/nlpcraft/examples/weather/weather_model.json") ). setQueryFunction(solver::solve). build() ); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy