| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF 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. 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 org.apache.nlpcraft.examples.weather; |
| |
| import com.google.gson.Gson; |
| import org.apache.commons.lang3.tuple.Pair; |
| import org.apache.nlpcraft.examples.misc.darksky.DarkSkyException; |
| import org.apache.nlpcraft.examples.misc.darksky.DarkSkyService; |
| import org.apache.nlpcraft.examples.misc.geo.keycdn.GeoManager; |
| import org.apache.nlpcraft.examples.misc.geo.keycdn.beans.GeoDataBean; |
| import org.apache.nlpcraft.model.*; |
| |
| import java.time.Instant; |
| import java.util.*; |
| |
| import static java.time.temporal.ChronoUnit.DAYS; |
| |
| /** |
| * Weather example data model. |
| * <p> |
| * This is a relatively complete weather service with JSON output and a non-trivial |
| * intent matching logic. It uses Apple's Dark Sky API weather provider REST service for the actual |
| * weather information (https://darksky.net/dev/docs#overview) |
| * <p> |
| * Note that this example uses class-based intent DSL to demonstrate its usage pattern. |
| * Note also that it also returns intent ID together with execution result which can be used in testing. |
| * <p> |
| * See 'README.md' file in the same folder for running and testing instructions. |
| */ |
| public class WeatherModel extends NCModelFileAdapter { |
| // Please register your own account at https://darksky.net/dev/docs/libraries and |
| // replace this demo token with your own. |
| private final DarkSkyService darkSky = new DarkSkyService("097e1aad75b22b88f494cf49211975aa", 31); |
| |
| // Geo manager. |
| private final GeoManager geoMrg = new GeoManager(); |
| |
| // Default shift in days for history and forecast. |
| private static final int DAYS_SHIFT = 5; |
| |
| // GSON instance. |
| private static final Gson GSON = new Gson(); |
| |
| // Keywords for 'local' weather. |
| private static final Set<String> LOCAL_WORDS = new HashSet<>(Arrays.asList("my", "local", "hometown")); |
| |
| /** |
| * Extracts geo location (city) from given solver context that is suitable for Dark Sky API weather service. |
| * |
| * @param ctx Intent solver context. |
| * @param geoTokOpt Optional geo token. |
| * @return Geo location. |
| */ |
| private Pair<Double, Double> prepGeo(NCIntentMatch ctx, Optional<NCToken> geoTokOpt) throws NCRejection { |
| if (geoTokOpt.isPresent()) { |
| NCToken geoTok = geoTokOpt.get(); |
| |
| Map<String, Object> cityMeta = geoTok.meta("nlpcraft:city:citymeta"); |
| |
| Double lat = (Double)cityMeta.get("latitude"); |
| Double lon = (Double)cityMeta.get("longitude"); |
| |
| if (lat == null || lon == null) { |
| String city = geoTok.meta("nlpcraft:city:city"); |
| |
| throw new NCRejection(String.format("Latitude and longitude not found for: %s", city)); |
| } |
| |
| return Pair.of(lat, lon); |
| } |
| |
| Optional<GeoDataBean> geoOpt = geoMrg.get(ctx.getContext().getRequest()); |
| |
| if (geoOpt.isEmpty()) |
| throw new NCRejection("City cannot be determined."); |
| |
| // 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. |
| |
| @SuppressWarnings("SuspiciousMethodCalls") |
| boolean hasLocalWord = |
| ctx.getVariant().stream().anyMatch(t -> LOCAL_WORDS.contains(t.meta("nlpcraft:nlp:origtext"))); |
| |
| if (hasLocalWord) |
| // Because we implicitly assume user's current city at this point we need to clear |
| // 'nlpcraft:city' tokens from conversation since they would no longer be valid. |
| ctx.getContext().getConversation().clearStm(t -> t.getId().equals("nlpcraft:city")); |
| |
| // Try current user location. |
| GeoDataBean geo = geoOpt.get(); |
| |
| return Pair.of(geo.getLatitude(), geo.getLongitude()); |
| } |
| |
| /** |
| * A callback for the intent match. |
| * |
| * @param ctx Intent match context. |
| * @param indToksOpt List of optional indicator elements. |
| * @param cityTokOpt Optional GEO token for city. |
| * @param dateTokOpt Optional date token. |
| * @return Callback result. |
| */ |
| @NCIntent( |
| "intent=req " + |
| "term~{id == 'wt:phen'}* " + // Zero or more weather phenomenon. |
| "term(ind)~{groups @@ 'indicator'}* " + // Optional indicator words (zero or more). |
| "term(city)~{id == 'nlpcraft:city'}? " + // Optional city. |
| "term(date)~{id == 'nlpcraft:date'}?" // Optional date (overrides indicator words). |
| ) |
| // NOTE: each samples group will reset conversation STM. |
| @NCIntentSample({ |
| "Current forecast?", |
| "Chance of rain in Berlin now?" |
| }) |
| // NOTE: each samples group will reset conversation STM. |
| @NCIntentSample({ |
| "Moscow forecast?", |
| "Chicago history" |
| }) |
| // NOTE: each samples group will reset conversation STM. |
| @NCIntentSample({ |
| "What's the local weather forecast?", |
| "What's the weather in Moscow?", |
| "What's the current forecast for Chicago?", |
| "What is the weather like outside?", |
| "How's the weather?", |
| "What's the weather forecast for the rest of the week?", |
| "What's the weather forecast this week?", |
| "What's the weather out there?", |
| "Is it cold outside?", |
| "Is it hot outside?", |
| "Will it rain today?", |
| "When it will rain in Delhi?", |
| "Is there any possibility of rain in Delhi?", |
| "Is it raining now?", |
| "Is there any chance of rain today?", |
| "Was it raining in Beirut last week?", |
| "How about yesterday?" |
| }) |
| public NCResult onMatch( |
| NCIntentMatch ctx, |
| @NCIntentTerm("ind") List<NCToken> indToksOpt, |
| @NCIntentTerm("city") Optional<NCToken> cityTokOpt, |
| @NCIntentTerm("date") Optional<NCToken> dateTokOpt |
| ) { |
| // Reject if intent match is not exact (at least one "dangling" token remain). |
| if (ctx.isAmbiguous()) |
| throw new NCRejection("Please clarify your request."); |
| |
| try { |
| Instant now = Instant.now(); |
| |
| Instant from = now; |
| Instant to = now; |
| |
| if (indToksOpt.stream().anyMatch(tok -> tok.getId().equals("wt:hist"))) |
| from = from.minus(DAYS_SHIFT, DAYS); |
| else if (indToksOpt.stream().anyMatch(tok -> tok.getId().equals("wt:fcast"))) |
| to = from.plus(DAYS_SHIFT, DAYS); |
| |
| if (dateTokOpt.isPresent()) { // Date token overrides any indicators. |
| NCToken dateTok = dateTokOpt.get(); |
| |
| from = Instant.ofEpochMilli(dateTok.meta("nlpcraft:date:from")); |
| to = Instant.ofEpochMilli(dateTok.meta("nlpcraft:date:to")); |
| } |
| |
| Pair<Double, Double> latLon = prepGeo(ctx, cityTokOpt); // Handles optional city too. |
| |
| double lat = latLon.getLeft(); |
| double lon = latLon.getRight(); |
| |
| return NCResult.json(GSON.toJson(from == to ? darkSky.getCurrent(lat, lon) : darkSky.getTimeMachine(lat, lon, from, to))); |
| } |
| catch (DarkSkyException e) { |
| throw new NCRejection(e.getLocalizedMessage()); |
| } |
| catch (NCRejection e) { |
| throw e; |
| } |
| catch (Exception e) { |
| throw new NCRejection("Weather provider error.", e); |
| } |
| } |
| |
| /** |
| * Loads the model. |
| */ |
| public WeatherModel() { |
| // Load model from external JSON file on classpath. |
| super("org/apache/nlpcraft/examples/weather/weather_model.json"); |
| } |
| |
| @Override |
| public void onDiscard() { |
| darkSky.stop(); |
| } |
| } |