diff --git a/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/duckduckgo/DuckDuckGoAgent.java b/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/duckduckgo/DuckDuckGoAgent.java new file mode 100644 index 0000000..5be7eb8 --- /dev/null +++ b/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/duckduckgo/DuckDuckGoAgent.java @@ -0,0 +1,231 @@ +/* + * Copyright 2017 The Johns Hopkins University Applied Physics Laboratory LLC + * All rights reserved. + * + * 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 edu.jhuapl.dorset.agents.duckduckgo; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import edu.jhuapl.dorset.ResponseStatus; +import edu.jhuapl.dorset.agents.AbstractAgent; +import edu.jhuapl.dorset.agents.AgentRequest; +import edu.jhuapl.dorset.agents.AgentResponse; +import edu.jhuapl.dorset.agents.Description; +import edu.jhuapl.dorset.http.HttpClient; +import edu.jhuapl.dorset.http.HttpRequest; +import edu.jhuapl.dorset.http.HttpResponse; +import edu.jhuapl.dorset.nlp.RuleBasedTokenizer; +import edu.jhuapl.dorset.nlp.Tokenizer; +import edu.jhuapl.dorset.sessions.Session; +import edu.jhuapl.dorset.sessions.Session.SessionStatus; + +/** + * DuckduckGo agent + * + * Duckduckgo has an instant answers API. It scrapes entity information from data sources like + * Wikipedia and CrunchBase. Documentation on the api here: https://duckduckgo.com/api + */ +public class DuckDuckGoAgent extends AbstractAgent { + private static final Logger logger = LoggerFactory.getLogger(DuckDuckGoAgent.class); + + private static final String SUMMARY = + "Get information about famous people, places, organizations."; + private static final String EXAMPLE = "Who is Barack Obama?"; + + private HttpClient client; + private SmartFormService smartFormService; + private int numFollowUpAttemptsThreshold = 2; + private int numFollowUpAttempts; + + private Set dictionary = new HashSet( + Arrays.asList("what", "who", "is", "are", "a", "an", "the")); + + /** + * Create a duckduckgo agent + * + * @param client http client + */ + public DuckDuckGoAgent(HttpClient client) { + this.client = client; + this.setDescription(new Description("general answers", SUMMARY, EXAMPLE)); + this.smartFormService = new SmartFormService(); + } + + @Override + public AgentResponse process(AgentRequest request) { + logger.debug("Handling the request: " + request.getText()); + String requestText = request.getText(); + Session session = request.getSession(); + + // check if session is a new session or a follow-up + AgentResponse agentResponse = null; + String entityText = extractEntity(requestText); + String data = null; + if (session != null) { + + if (session.getSessionStatus() == SessionStatus.NEW) { + data = requestData(entityText); + agentResponse = createResponse(data); + agentResponse.setSession(session); + + } else if (session.getSessionStatus() == SessionStatus.OPEN) { + this.numFollowUpAttempts = this.numFollowUpAttempts + 1; + String smartResponse = this.smartFormService.querySmartFormHistory(session.getId(), + entityText); + + if (smartResponse != null) { + agentResponse = new AgentResponse(smartResponse); + agentResponse.setSessionStatus(SessionStatus.CLOSED); + agentResponse.setSession(session); + return agentResponse; + } else { + // could not find answer in history + + if (this.numFollowUpAttempts < this.numFollowUpAttemptsThreshold) { + // send the disambiguation response again + String disambiguationResponse = + this.smartFormService.getLastDisambiguationResponse(); + + ResponseStatus responseStatus = + new ResponseStatus(ResponseStatus.Code.NEEDS_REFINEMENT, + "I am sorry, I am still unsure what you are asking about." + + " The options are: " + + disambiguationResponse); + agentResponse = new AgentResponse(responseStatus); + agentResponse.setSessionStatus(SessionStatus.OPEN); + agentResponse.setSession(session); + + return agentResponse; + + } else { + // send could not answer request message + ResponseStatus responseStatus = new ResponseStatus( + ResponseStatus.Code.AGENT_DID_NOT_KNOW_ANSWER, + "The agent did not know the answer. Session is now closed, please try again."); + agentResponse = new AgentResponse(responseStatus); + agentResponse.setSessionStatus(SessionStatus.CLOSED); + agentResponse.setSession(session); + + return agentResponse; + } + } + } + } else { + data = requestData(entityText); + agentResponse = createResponse(data); + } + this.smartFormService.updateHistory(request, data); + this.numFollowUpAttempts = 0; + return agentResponse; + } + + protected String requestData(String entity) { + HttpResponse response = client.execute(HttpRequest.get(createUrl(entity))); + if (response == null || response.isError()) { + return null; + } + return response.asString(); + } + + protected AgentResponse createResponse(String json) { + Gson gson = new Gson(); + JsonObject jsonObj = gson.fromJson(json, JsonObject.class); + String heading = jsonObj.get("Heading").getAsString(); + if (heading.equals("")) { + // duckduckgo does not know + AgentResponse agentResponse = + new AgentResponse(ResponseStatus.Code.AGENT_DID_NOT_KNOW_ANSWER); + agentResponse.setSessionStatus(SessionStatus.CLOSED); + return agentResponse; + } + String abstractText = jsonObj.get("AbstractText").getAsString(); + if (abstractText.equals("")) { + // most likely a disambiguation page + List potentialEntities = + this.smartFormService.getCurrentExchangePotentialEntities(json); + String disambiguationResponse = + this.smartFormService.formatDisambiguationResponse(potentialEntities); + + ResponseStatus responseStatus = new ResponseStatus(ResponseStatus.Code.NEEDS_REFINEMENT, + "Multiple answers for this question. " + disambiguationResponse); + + AgentResponse agentResponse = new AgentResponse(responseStatus); + agentResponse.setSessionStatus(SessionStatus.OPEN); + + return agentResponse; + } + AgentResponse agentResponse = new AgentResponse(abstractText); + agentResponse.setSessionStatus(SessionStatus.CLOSED); + return agentResponse; + } + + /** + * Iterate over the words until we think we get to the name of the entity + */ + protected String extractEntity(String sentence) { + Tokenizer tokenizer = new RuleBasedTokenizer(true); + String[] words = tokenizer.tokenize(sentence); + int index = 0; + for (index = 0; index < words.length; index++) { + if (!dictionary.contains(words[index].toLowerCase())) { + break; + } + } + return joinStrings(Arrays.copyOfRange(words, index, words.length), " "); + } + + protected String joinStrings(String[] strings, String separator) { + if (strings == null || strings.length == 0) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + sb.append(strings[0]); + for (int i = 1; i < strings.length; i++) { + sb.append(separator); + sb.append(strings[i]); + } + return sb.toString(); + } + + protected static String createUrl(String entity) { + try { + entity = URLEncoder.encode(entity, "UTF-8"); + } catch (UnsupportedEncodingException e) { + // this isn't going to happen + logger.error("Unexpected exception when encoding url", e); + } + return "http://api.duckduckgo.com/?format=json&q=" + entity; + } + + public int getNumFollowUpAttemptsThreshold() { + return this.numFollowUpAttemptsThreshold; + } + + public void setNumFollowUpAttemptsThreshold(int numFollowUpAttemptsThreshold) { + this.numFollowUpAttemptsThreshold = numFollowUpAttemptsThreshold; + } +} diff --git a/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/duckduckgo/DuckDuckGoSmartForm.java b/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/duckduckgo/DuckDuckGoSmartForm.java new file mode 100644 index 0000000..2754349 --- /dev/null +++ b/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/duckduckgo/DuckDuckGoSmartForm.java @@ -0,0 +1,87 @@ +package edu.jhuapl.dorset.agents.duckduckgo; + +import java.util.Date; +import java.util.List; + +import com.google.gson.JsonObject; + +import edu.jhuapl.dorset.agents.AgentRequest; + +public class DuckDuckGoSmartForm { + + public String sessionId; + public Date timestamp; + public String requestText; + public List relatedTopics; + public String abstractText; + + /** + * + * DuckDuckGoSmartForm + * + * + */ + public DuckDuckGoSmartForm() { + + } + + /** + * + * DuckDuckGoSmartForm + * + * @param request request + * @param relatedTopics related topics returned from DuckDuckGo given request + * + */ + public DuckDuckGoSmartForm(AgentRequest request, List relatedTopics) { + try { + this.sessionId = request.getSession().getId(); + } catch (NullPointerException e) { + this.sessionId = null; + } + this.requestText = request.getText(); + this.relatedTopics = relatedTopics; + + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public Date getTimestamp() { + return this.timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public String getRequestText() { + return requestText; + } + + public void setRequestText(String requestText) { + this.requestText = requestText; + } + + public List getRelatedTopics() { + return relatedTopics; + } + + public void setRelatedTopics(List relatedTopics) { + this.relatedTopics = relatedTopics; + } + + public String getAbstractText() { + return abstractText; + } + + public void setAbstractText(String abstractText) { + this.abstractText = abstractText; + } + +} diff --git a/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/duckduckgo/SmartFormService.java b/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/duckduckgo/SmartFormService.java new file mode 100644 index 0000000..84a3a25 --- /dev/null +++ b/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/duckduckgo/SmartFormService.java @@ -0,0 +1,288 @@ +package edu.jhuapl.dorset.agents.duckduckgo; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import edu.jhuapl.dorset.agents.AgentRequest; + +public class SmartFormService { + + public int historyThreshold = 20; + public int numRelatedTopicsThreshold = 5; + public List smartFormHistory; + + /** + * + * SmartFormService + * + */ + public SmartFormService() { + this.smartFormHistory = new ArrayList(); + } + + /** + * Get history threshold + * + * @return historyThreshold + * + */ + public int getHistoryThreshold() { + return historyThreshold; + } + + /** + * Set history threshold + * + * @param historyThreshold + * + */ + public void setHistoryThreshold(int historyThreshold) { + this.historyThreshold = historyThreshold; + } + + /** + * Get number of related topics threshold + * + * @return numRelatedTopicsThreshold + * + */ + public int getNumRelatedTopicsThreshold() { + return numRelatedTopicsThreshold; + } + + /** + * Set number of related topics threshold + * + * @param numRelatedTopicsThreshold + * + */ + public void setNumRelatedTopicsThreshold(int numRelatedTopicsThreshold) { + this.numRelatedTopicsThreshold = numRelatedTopicsThreshold; + } + + /** + * Update history + * + * @param request the request + * @param data data returned from DuckDuckGo given request + * + */ + public void updateHistory(AgentRequest request, String data) { + List relatedTopics = extractDdgData(data, this.numRelatedTopicsThreshold); + + DuckDuckGoSmartForm ddgSmartForm = new DuckDuckGoSmartForm(request, relatedTopics); + + if (this.smartFormHistory.size() < this.historyThreshold) { + this.smartFormHistory.add(ddgSmartForm); + } else if (this.smartFormHistory.size() >= this.historyThreshold) { + this.smartFormHistory.remove(this.smartFormHistory.size() - 1); + this.smartFormHistory.add(ddgSmartForm); + } + + } + + // rename + /** + * Query smart form history + * + * @param sessionId the session id + * @param entityText text of interest + * + */ + public String querySmartFormHistory(String sessionId, String entityText) { // rename? + String smartResponse; + + ArrayList> distances = new ArrayList>(); + double distance; + double maxDistance = 0.0; + Map maxDistanceIndex = new HashMap(); + + // populate distance matrix + for (int i = 0; i < this.smartFormHistory.size(); i++) { + ArrayList innerDistances = new ArrayList(); + + if (this.smartFormHistory.get(i).getSessionId().equals(sessionId)) { + + List relatedTopics = this.smartFormHistory + .get(this.smartFormHistory.size() - 1).getRelatedTopics(); + + // iterate over all related topics and find closest match + for (int j = 0; j < relatedTopics.size(); j++) { + String relatedTopic = (relatedTopics.get(j).get("relatedTopic").getAsString()); + distance = diceCoefficient(entityText.toLowerCase(), + relatedTopic.toLowerCase()); + + if (distance > maxDistance) { + maxDistance = distance; + maxDistanceIndex.clear(); + maxDistanceIndex.put(i, j); + } + + innerDistances.add(distance); + } + } + distances.add(innerDistances); + } + + if (maxDistance == 0.0) { + smartResponse = null; + } else { + int index = maxDistanceIndex.keySet().iterator().next(); + List relatedTopics = this.smartFormHistory.get(index).getRelatedTopics(); + smartResponse = (relatedTopics.get(maxDistanceIndex.get(index)).get("relatedTopicText").getAsString()); + + } + + return smartResponse; + + } + + /** + * Calculate dice coefficient + * cite: https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Dice%27s_coefficient + * + * @param s1 string one + * @param s2 string two + * @return diceCoefficient dice coefficient for s1 and s2 + * + */ + public static double diceCoefficient(String s1, String s2) { + Set nx = new HashSet(); + Set ny = new HashSet(); + + for (int i = 0; i < s1.length() - 1; i++) { + char x1 = s1.charAt(i); + char x2 = s1.charAt(i + 1); + String tmp = "" + x1 + x2; + nx.add(tmp); + } + for (int j = 0; j < s2.length() - 1; j++) { + char y1 = s2.charAt(j); + char y2 = s2.charAt(j + 1); + String tmp = "" + y1 + y2; + ny.add(tmp); + } + + Set intersection = new HashSet(nx); + intersection.retainAll(ny); + double totcombigrams = intersection.size(); + + return (2 * totcombigrams) / (nx.size() + ny.size()); + } + + /** + * Get current exchange potential entities + * + * @param data data returned from DuckDuckGo given request + * @return potentialEntities + * + */ + public List getCurrentExchangePotentialEntities(String data) { + List potentialEntities = new ArrayList(); + + List relatedTopics = extractDdgData(data, this.numRelatedTopicsThreshold); + for (int index = 0; index < relatedTopics.size(); index++) { + potentialEntities.add(relatedTopics.get(index).get("relatedTopic").getAsString()); + } + return potentialEntities; + } + + /** + * Get current exchange potential entities + * + * @param relatedTopics list of related topics extracted from DuckDuckGo given request + * @return potentialEntities + * + */ + public List getCurrentExchangePotentialEntities(List relatedTopics) { + List potentialEntities = new ArrayList(); + + for (int index = 0; index < relatedTopics.size(); index++) { + potentialEntities.add(relatedTopics.get(index).get("relatedTopic").getAsString()); + } + return potentialEntities; + } + /** + * Extract duckduckgo data response + * + * @param data data returned from DuckDuckGo given request + * @param numRelatedTopicsThreshold threshold for number of related topics + * @return relatedTopic list of objects containing related topic information + * + */ + public List extractDdgData(String data, int numRelatedTopicsThreshold) { + List relatedTopics = new ArrayList(); + + Gson gson = new Gson(); + + JsonObject jsonObj = gson.fromJson(data, JsonObject.class); + + JsonArray relatedTopicsArr = jsonObj.get("RelatedTopics").getAsJsonArray(); + + for (int index = 0; index < relatedTopicsArr.size(); index++) { + if (index < numRelatedTopicsThreshold) { + + if (relatedTopicsArr.get(index).getAsJsonObject().get("Result") != null) { + + String relatedTopicUrl = relatedTopicsArr.get(index).getAsJsonObject() + .get("FirstURL").getAsString(); + String relatedTopicText = relatedTopicsArr.get(index).getAsJsonObject() + .get("Text").getAsString(); + + // parse relatedTopicUrl 'https://duckduckgo.com/Donald_Trump' + String[] tokenizedUrl = relatedTopicUrl.split("/"); + String relatedTopic = tokenizedUrl[3].replaceAll("_", " "); + relatedTopic = relatedTopic.replaceAll("%2C", ","); + + JsonObject relatedTopicJsonObj = new JsonObject(); + relatedTopicJsonObj.addProperty("relatedTopic", relatedTopic); + relatedTopicJsonObj.addProperty("relatedTopicText", relatedTopicText); + relatedTopicJsonObj.addProperty("relatedTopicURL", relatedTopicUrl); + + relatedTopics.add(relatedTopicJsonObj); + + } + } + } + + return relatedTopics; + } + + /** + * + * + */ + public String formatDisambiguationResponse(List potentialEntities) { + String response = "Did you mean "; + for (int index = 0; index < potentialEntities.size(); index++) { + if (index != potentialEntities.size() - 1) { + response = response + "'" + potentialEntities.get(index) + "', "; + + } else { + response = response + " or '" + potentialEntities.get(index) + "'?"; + } + } + return response; + } + + /** + * + * + */ + public String getLastDisambiguationResponse() { + List relatedTopics = this.smartFormHistory.get(this.smartFormHistory.size() - 1).getRelatedTopics(); + List potentialEntities = + this.getCurrentExchangePotentialEntities(relatedTopics); + String disambiguationReponse = + this.formatDisambiguationResponse(potentialEntities); + return disambiguationReponse; + } +} diff --git a/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/stockagent/CompanyInfo.java b/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/stockagent/CompanyInfo.java new file mode 100644 index 0000000..c270d8c --- /dev/null +++ b/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/stockagent/CompanyInfo.java @@ -0,0 +1,112 @@ +/* + * Copyright 2016 The Johns Hopkins University Applied Physics Laboratory LLC + * All rights reserved. + * + * 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 edu.jhuapl.dorset.agents.stockagent; + +public class CompanyInfo { + protected String symbol; + protected String name; + protected String lastSale; + protected String marketCap; + protected String ipoYear; + protected String sector; + protected String industry; + protected String summary; + + public CompanyInfo() { + } + + /** + * Company information for Stock Agent + * + */ + public CompanyInfo(CompanyInfo companyInfo) { + this.symbol = companyInfo.symbol; + this.name = companyInfo.name; + this.lastSale = companyInfo.lastSale; + this.marketCap = companyInfo.marketCap; + this.ipoYear = companyInfo.ipoYear; + this.sector = companyInfo.sector; + this.industry = companyInfo.industry; + this.summary = companyInfo.summary; + } + + public String getSymbol() { + return symbol; + } + + public void setSymbol(String symbol) { + this.symbol = symbol; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLastSale() { + return lastSale; + } + + public void setLastSale(String lastSale) { + this.lastSale = lastSale; + } + + public String getMarketCap() { + return marketCap; + } + + public void setMarketCap(String marketCap) { + this.marketCap = marketCap; + } + + public String getIpoYear() { + return ipoYear; + } + + public void setIpoYear(String ipoYear) { + this.ipoYear = ipoYear; + } + + public String getSector() { + return sector; + } + + public void setSector(String sector) { + this.sector = sector; + } + + public String getIndustry() { + return industry; + } + + public void setIndustry(String industry) { + this.industry = industry; + } + + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + +} diff --git a/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/stockagent/StockAgent.java b/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/stockagent/StockAgent.java new file mode 100644 index 0000000..6e75df2 --- /dev/null +++ b/agents/web-api/src/main/java/edu/jhuapl/dorset/agents/stockagent/StockAgent.java @@ -0,0 +1,284 @@ +/* + * Copyright 2016 The Johns Hopkins University Applied Physics Laboratory LLC + * All rights reserved. + * + * 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 edu.jhuapl.dorset.agents.stockagent; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.supercsv.cellprocessor.Optional; +import org.supercsv.cellprocessor.constraint.NotNull; +import org.supercsv.cellprocessor.ift.CellProcessor; +import org.supercsv.io.CsvBeanReader; +import org.supercsv.io.ICsvBeanReader; +import org.supercsv.prefs.CsvPreference; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import edu.jhuapl.dorset.Response; +import edu.jhuapl.dorset.ResponseStatus; +import edu.jhuapl.dorset.agents.AbstractAgent; +import edu.jhuapl.dorset.agents.AgentRequest; +import edu.jhuapl.dorset.agents.AgentResponse; +import edu.jhuapl.dorset.agents.Description; +import edu.jhuapl.dorset.http.HttpClient; +import edu.jhuapl.dorset.http.HttpRequest; +import edu.jhuapl.dorset.http.HttpResponse; + +public class StockAgent extends AbstractAgent { + + private static final Logger logger = LoggerFactory.getLogger(StockAgent.class); + + private static final String SUMMARY = "Stock ticker that gives historical stock information for NASDAQ and NYSE companies"; + private static final String [] EXAMPLE = new String[] {"Stocks Facebook", "Facebook"}; + + private static final CellProcessor[] processors = new CellProcessor[] { + new NotNull(), new NotNull(), new NotNull(), new Optional(), + new Optional(), new Optional(), new NotNull(), new Optional() }; + + private String baseurl = "https://www.quandl.com/api/v3/datasets/WIKI/"; + private String apiKey; + private HttpClient client; + private TreeMap stockSymbolMap; + private static final int DAYS_IN_A_MONTH = 30; + + + /** + * Stock agent + * + * The Quandl API provides stock information for various companies world + * wide. The StockAgent uses said API to scrape historical closing prices of + * NASDAQ and NYSE companies. + * + * @param client An http client object + * @param apiKey A Quandl API key + */ + public StockAgent(HttpClient client, String apiKey) { + this.client = client; + this.apiKey = apiKey; + + this.stockSymbolMap = new TreeMap(String.CASE_INSENSITIVE_ORDER); + readCsvFile("stockagent/NASDAQ_Companies.csv"); + readCsvFile("stockagent/NYSE_Companies.csv"); + + this.setDescription(new Description("stock ticker", SUMMARY, EXAMPLE)); + + } + + @Override + public AgentResponse process(AgentRequest request) { + logger.debug("Handling the request: " + request.getText()); + + // remove trigger word "stocks" + String regex = "\\bstocks\\b"; + Pattern pat = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + String requestCompanyName = pat.matcher(request.getText()).replaceAll("").trim(); + + CompanyInfo stockCompanyInfo = findStockSymbol(requestCompanyName); + + if (stockCompanyInfo == null) { + return new AgentResponse(new ResponseStatus(ResponseStatus.Code.AGENT_DID_NOT_KNOW_ANSWER, + "I am sorry, I don't understand which company you are asking about.")); + } + + String keywordCompanyName = stockCompanyInfo.getName(); + String keywordCompanySymbol = stockCompanyInfo.getSymbol(); + + String json = null; + + json = requestData(keywordCompanySymbol); + + if (json == null) { + + // replace ".." with "." to maintain proper grammar when the + // keyword contains an abbreviation + return new AgentResponse(new ResponseStatus(ResponseStatus.Code.AGENT_DID_NOT_KNOW_ANSWER, + ("I am sorry, I can't find the proper stock data for the company " + + keywordCompanyName + ".").replace("..", "."))); + } + + //See examples of the Json data returned by the API in src/test/resources/stockAgent + JsonObject returnObj = processData(json, keywordCompanyName); + + // replace ".." with "." to maintain proper grammar when the + // keyword contains an abbreviation + if (returnObj != null) { + return new AgentResponse(Response.Type.JSON, + ("Here is the longitudinal stock market data from the last " + + DAYS_IN_A_MONTH + + " days for " + + keywordCompanyName + ".").replace("..", "."), + returnObj.toString()); + } + return new AgentResponse(ResponseStatus.Code.AGENT_DID_NOT_KNOW_ANSWER); + + } + + protected CompanyInfo findStockSymbol(String stockCompanyName) { + CompanyInfo companyInfo = null; + ArrayList regexMatches = new ArrayList(); + + if (this.stockSymbolMap.get(stockCompanyName) != null) { + companyInfo = this.stockSymbolMap.get(stockCompanyName); + + } else { + String regex = "\\b" + stockCompanyName + "\\b"; + + Pattern pat = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + + for (Map.Entry entry : stockSymbolMap + .entrySet()) { + Matcher matcher = pat.matcher(entry.getKey()); + + if (matcher.find()) { + regexMatches.add(entry.getKey()); + } + } + + if (regexMatches.size() == 0) { + companyInfo = null; + } else if (regexMatches.size() == 1) { + companyInfo = this.stockSymbolMap.get(regexMatches.get(0)); + + } else { + int distance; + HashMap matchDistanceMap = new HashMap(); + for (int i = 0; i < regexMatches.size(); i++) { + distance = (StringUtils.getLevenshteinDistance(regexMatches.get(i), stockCompanyName)); + matchDistanceMap.put(regexMatches.get(i), distance); + } + + Entry minDistancePair = null; + for (Entry entry : matchDistanceMap.entrySet()) { + if (minDistancePair == null || minDistancePair.getValue() > entry.getValue()) { + minDistancePair = entry; + } + } + + companyInfo = this.stockSymbolMap.get(minDistancePair.getKey()); + + } + + } + + return companyInfo; + } + + protected JsonObject processData(String json, String keyWordCompanyName) { + + Gson gson = new Gson(); + JsonObject returnObj = new JsonObject(); + JsonObject jsonObj = gson.fromJson(json, JsonObject.class); + + if (jsonObj != null) { + + if ((jsonObj.get("dataset")) != null) { + JsonArray jsonDataArray = (JsonArray) (((JsonObject) jsonObj.get("dataset")).get("data")); + + ArrayList responseDataArrayList = new ArrayList<>(); + ArrayList responseLabelsArrayList = new ArrayList<>(); + + for (int i = 0; i < jsonDataArray.size(); i++) { + JsonArray jsonDataArrayNested = (JsonArray) (jsonDataArray.get(i)); + responseDataArrayList.add(jsonDataArrayNested.get(4)); + responseLabelsArrayList.add(jsonDataArrayNested.get(0)); + } + Collections.reverse(responseDataArrayList); + Collections.reverse(responseLabelsArrayList); + + List returnDataJsonList = responseDataArrayList + .subList(responseDataArrayList.size() - DAYS_IN_A_MONTH, + responseDataArrayList.size()); + + + JsonArray returnDataJsonListStr = new JsonArray(); + for (int i = 0 ; i < returnDataJsonList.size(); i++) { + returnDataJsonListStr.add(returnDataJsonList.get(i)); + } + + JsonObject jsonData = new JsonObject(); + jsonData.add(keyWordCompanyName, returnDataJsonListStr); + + returnObj.addProperty("data", jsonData.toString()); + + List returnLabelsJsonList = responseLabelsArrayList + .subList(responseLabelsArrayList.size() - DAYS_IN_A_MONTH, + responseLabelsArrayList.size()); + + returnObj.addProperty("labels", returnLabelsJsonList.toString()); + + returnObj.addProperty("title", keyWordCompanyName + " Stock Ticker"); + returnObj.addProperty("xaxis", "Day"); + returnObj.addProperty("yaxis", "Close of day market price ($)"); + returnObj.addProperty("plotType", "lineplot"); + + } + + } + + return returnObj; + } + + protected String requestData(String keyword) { + String url = this.baseurl + keyword + ".json?api_key=" + apiKey; + HttpResponse response = client.execute(HttpRequest.get(url)); + if (response.isSuccess()) { + return response.asString(); + } else { + return null; + } + } + + protected void readCsvFile(String filename) { + InputStream companiesCsvInput = StockAgent.class.getClassLoader() + .getResourceAsStream(filename); + + ICsvBeanReader csvBeanReader = null; + + try { + csvBeanReader = new CsvBeanReader(new BufferedReader( + new InputStreamReader(companiesCsvInput)), + CsvPreference.STANDARD_PREFERENCE); + final String[] header = csvBeanReader.getHeader(true); + CompanyInfo companyInfo; + while ((companyInfo = csvBeanReader.read(CompanyInfo.class, header, processors)) != null) { + this.stockSymbolMap.put(companyInfo.getName(), companyInfo); + } + csvBeanReader.close(); + } catch (IOException e) { + logger.error("Failed to load " + filename + ".", e); + } + } + +} diff --git a/agents/web-api/src/test/java/edu/jhuapl/dorset/agents/duckduckgo/DuckDuckGoAgentTest.java b/agents/web-api/src/test/java/edu/jhuapl/dorset/agents/duckduckgo/DuckDuckGoAgentTest.java new file mode 100644 index 0000000..8627a01 --- /dev/null +++ b/agents/web-api/src/test/java/edu/jhuapl/dorset/agents/duckduckgo/DuckDuckGoAgentTest.java @@ -0,0 +1,174 @@ +/* + * Copyright 2017 The Johns Hopkins University Applied Physics Laboratory LLC + * All rights reserved. + * + * 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 edu.jhuapl.dorset.agents.duckduckgo; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import edu.jhuapl.dorset.ResponseStatus; +import edu.jhuapl.dorset.agents.Agent; +import edu.jhuapl.dorset.agents.AgentRequest; +import edu.jhuapl.dorset.agents.AgentResponse; +import edu.jhuapl.dorset.agents.FakeHttpClient; +import edu.jhuapl.dorset.agents.FakeHttpResponse; +import edu.jhuapl.dorset.agents.FileReader; +import edu.jhuapl.dorset.http.HttpClient; +import edu.jhuapl.dorset.sessions.Session; +import edu.jhuapl.dorset.sessions.Session.SessionStatus; + +public class DuckDuckGoAgentTest { + + @Test + public void testGetGoodResponse() { + String query = "Barack Obama"; + String jsonData = FileReader.getFileAsString("duckduckgo/barack_obama.json"); + HttpClient client = new FakeHttpClient(new FakeHttpResponse(jsonData)); + + Agent agent = new DuckDuckGoAgent(client); + AgentResponse response = agent.process(new AgentRequest(query)); + + assertTrue(response.isSuccess()); + assertTrue(response.getText() + .startsWith("Barack Hussein Obama II is an American politician")); + } + + @Test + public void testWithFullSentence() { + String jsonData = FileReader.getFileAsString("duckduckgo/barack_obama.json"); + HttpClient client = new FakeHttpClient(new FakeHttpResponse(jsonData)); + + Agent agent = new DuckDuckGoAgent(client); + AgentResponse response = agent.process(new AgentRequest("Who is Barack Obama?")); + + assertTrue(response.isSuccess()); + assertTrue(response.getText() + .startsWith("Barack Hussein Obama II is an American politician")); + } + + @Test + public void testGetDisambiguationResponse() { + String query = "Obama"; + String jsonData = FileReader.getFileAsString("duckduckgo/obama.json"); + HttpClient client = new FakeHttpClient(new FakeHttpResponse(jsonData)); + + Agent agent = new DuckDuckGoAgent(client); + AgentResponse response = agent.process(new AgentRequest(query)); + + assertEquals(ResponseStatus.Code.NEEDS_REFINEMENT, response.getStatus().getCode()); + assertEquals("Multiple answers for this question. Did you mean 'Barack Obama', 'Obama, Fukui', " + + " or 'Obama Day'?", response.getStatus().getMessage()); + assertEquals(SessionStatus.OPEN, response.getSessionStatus()); + + } + + @Test + public void testGetEmptyResponse() { + String query = "zergblah"; + String jsonData = FileReader.getFileAsString("duckduckgo/zergblah.json"); + HttpClient client = new FakeHttpClient(new FakeHttpResponse(jsonData)); + + Agent agent = new DuckDuckGoAgent(client); + AgentResponse response = agent.process(new AgentRequest(query)); + + assertEquals(ResponseStatus.Code.AGENT_DID_NOT_KNOW_ANSWER, response.getStatus().getCode()); + } + + @Test + public void testUrlEncoding() { + String urlBase = "http://api.duckduckgo.com/?format=json&q="; + assertEquals(urlBase + "Barack+Obama", DuckDuckGoAgent.createUrl("Barack Obama")); + } + + @Test + public void testFollowOnResponseWithSession() { + String firstQuery = "Obama"; + String secondQuery = "Barack Obama"; + String jsonData = FileReader.getFileAsString("duckduckgo/obama.json"); + HttpClient client = new FakeHttpClient(new FakeHttpResponse(jsonData)); + + Session session = new Session("1"); + session.setSessionStatus(SessionStatus.NEW); + + Agent agent = new DuckDuckGoAgent(client); + AgentRequest agentRequest = new AgentRequest(firstQuery); + agentRequest.setSession(session); + AgentResponse response = agent.process(agentRequest); + + assertEquals(ResponseStatus.Code.NEEDS_REFINEMENT, response.getStatus().getCode()); + assertEquals("Multiple answers for this question. Did you mean 'Barack Obama', " + + "'Obama, Fukui', or 'Obama Day'?", response.getStatus().getMessage()); + assertEquals(SessionStatus.OPEN, response.getSessionStatus()); + + session.setSessionStatus(SessionStatus.OPEN); + + AgentRequest secondAgentRequest = new AgentRequest(secondQuery); + secondAgentRequest.setSession(session); + + response = agent.process(secondAgentRequest); + + assertEquals("Barack Obama The 44th and current President of the United States, " + + "as well as the first African American to...", response.getText()); + assertEquals(SessionStatus.CLOSED, response.getSessionStatus()); + } + + @Test + public void testMultipleFollowOnResponseWithSession() { + String firstQuery = "Obama"; + String followUpQuery = "off-topic request"; + String jsonData = FileReader.getFileAsString("duckduckgo/obama.json"); + HttpClient client = new FakeHttpClient(new FakeHttpResponse(jsonData)); + + Session session = new Session("1"); + session.setSessionStatus(SessionStatus.NEW); + + DuckDuckGoAgent agent = new DuckDuckGoAgent(client); + int numFollowUpAttemptsThreshold = 2; + agent.setNumFollowUpAttemptsThreshold(numFollowUpAttemptsThreshold); + + AgentRequest agentRequest = new AgentRequest(firstQuery); + agentRequest.setSession(session); + AgentResponse response = agent.process(agentRequest); + + assertEquals(ResponseStatus.Code.NEEDS_REFINEMENT, response.getStatus().getCode()); + assertEquals("Multiple answers for this question. Did you mean 'Barack Obama', " + + "'Obama, Fukui', or 'Obama Day'?", response.getStatus().getMessage()); + assertEquals(SessionStatus.OPEN, response.getSessionStatus()); + + session.setSessionStatus(SessionStatus.OPEN); + for (int i = 0; i < numFollowUpAttemptsThreshold - 1; i++){ + AgentRequest followUpAgentRequest = new AgentRequest(followUpQuery); + followUpAgentRequest.setSession(session); + response = agent.process(followUpAgentRequest); + + assertEquals(ResponseStatus.Code.NEEDS_REFINEMENT, response.getStatus().getCode()); + assertEquals("I am sorry, I am still unsure what you are asking about. The options are: " + + "Did you mean 'Barack Obama', 'Obama, Fukui', or 'Obama Day'?", response.getStatus().getMessage()); + assertEquals(SessionStatus.OPEN, response.getSessionStatus()); + } + + AgentRequest followUpAgentRequest = new AgentRequest(followUpQuery); + followUpAgentRequest.setSession(session); + response = agent.process(followUpAgentRequest); + + assertEquals(ResponseStatus.Code.AGENT_DID_NOT_KNOW_ANSWER, response.getStatus().getCode()); + assertEquals("The agent did not know the answer. Session is now closed, " + + "please try again.", response.getStatus().getMessage()); + assertEquals(SessionStatus.CLOSED, response.getSessionStatus()); + } +} diff --git a/agents/web-api/src/test/java/edu/jhuapl/dorset/agents/stockagent/StockAgentTest.java b/agents/web-api/src/test/java/edu/jhuapl/dorset/agents/stockagent/StockAgentTest.java new file mode 100644 index 0000000..8f6d801 --- /dev/null +++ b/agents/web-api/src/test/java/edu/jhuapl/dorset/agents/stockagent/StockAgentTest.java @@ -0,0 +1,149 @@ +/** + * Copyright 2016 The Johns Hopkins University Applied Physics Laboratory LLC + * All rights reserved. + * + * 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 edu.jhuapl.dorset.agents.stockagent; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Scanner; + +import org.junit.Test; + +import edu.jhuapl.dorset.agents.Agent; +import edu.jhuapl.dorset.agents.AgentRequest; +import edu.jhuapl.dorset.agents.AgentResponse; +import edu.jhuapl.dorset.agents.FakeHttpClient; +import edu.jhuapl.dorset.agents.FakeHttpResponse; +import edu.jhuapl.dorset.http.HttpClient; +import edu.jhuapl.dorset.http.HttpRequest; +import edu.jhuapl.dorset.http.HttpResponse; + +public class StockAgentTest { + + private String apikey = "default_apikey"; + + protected String getJsonData(String filename) { + ClassLoader classLoader = StockAgent.class.getClassLoader(); + URL url = classLoader.getResource(filename); + try { + Path path = Paths.get(url.toURI()); + try (Scanner scanner = new Scanner(new File(path.toString()))) { + return scanner.useDelimiter("\\Z").next(); + } + } catch (URISyntaxException | FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + @Test + public void testStockAgentExactMatch() { + String keyword = "facebook, Inc."; + String jsonData = getJsonData("stockagent/MockJson_Facebook.json"); + HttpClient client = new FakeHttpClient(new FakeHttpResponse(jsonData)); + + Agent stocks = new StockAgent(client, apikey); + AgentRequest request = new AgentRequest("Stocks " + keyword); + AgentResponse response = stocks.process(request); + + assertEquals("Here is the longitudinal stock market data from the last 30 days for Facebook, Inc.", + response.getText()); + } + + @Test + public void testStockAgentCloseMatch() { + String keyword = "facebook"; + + String jsonData = getJsonData("stockagent/MockJson_Facebook.json"); + HttpClient client = new FakeHttpClient(new FakeHttpResponse(jsonData)); + + Agent stocks = new StockAgent(client, apikey); + AgentRequest request = new AgentRequest(keyword); + AgentResponse response = stocks.process(request); + + assertEquals("Here is the longitudinal stock market data from the last 30 days for Facebook, Inc.", + response.getText()); + } + + @Test + public void testStockAgentExactMatch2() { + String keyword = "Apple inc."; + + String jsonData = getJsonData("stockagent/MockJson_Apple.json"); + HttpClient client = new FakeHttpClient(new FakeHttpResponse(jsonData)); + + Agent stocks = new StockAgent(client, apikey); + AgentRequest request = new AgentRequest(keyword); + AgentResponse response = stocks.process(request); + assertEquals("Here is the longitudinal stock market data from the last 30 days for Apple Inc.", + response.getText()); + } + + @Test + public void testStockAgentCloseMatch2() { + String keyword = "apple"; + + String jsonData = getJsonData("stockagent/MockJson_Apple.json"); + HttpClient client = new FakeHttpClient(new FakeHttpResponse(jsonData)); + + Agent stocks = new StockAgent(client, apikey); + AgentRequest request = new AgentRequest(keyword); + AgentResponse response = stocks.process(request); + + assertEquals("Here is the longitudinal stock market data from the last 30 days for Apple Inc.", + response.getText()); + } + + @Test + public void testStockAgentNoQuandlData() { + String keyword = "First Bank"; // maps to FRBA in NASDAQ + String jsonData = getJsonData("stockagent/MockJson_404.json"); + HttpResponse httpResponse = mock(HttpResponse.class); + when(httpResponse.isSuccess()).thenReturn(false); + when(httpResponse.asString()).thenReturn(jsonData); + HttpClient client = mock(HttpClient.class); + when(client.execute(any(HttpRequest.class))).thenReturn(httpResponse); + + Agent stocks = new StockAgent(client, apikey); + AgentRequest request = new AgentRequest(keyword); + AgentResponse response = stocks.process(request); + + assertEquals("I am sorry, I can't find the proper stock data for the company " + + keyword + ".", response.getStatus().getMessage()); + } + + @Test + public void testStockAgentFailure() { + String keyword = "company that does not exist"; + HttpClient client = mock(HttpClient.class); + + Agent stocks = new StockAgent(client, apikey); + AgentRequest request = new AgentRequest(keyword); + AgentResponse response = stocks.process(request); + + assertEquals("I am sorry, I don't understand which company you are asking about.", + response.getStatus().getMessage()); + } + +} diff --git a/components/pom.xml b/components/pom.xml index d894119..04dc247 100644 --- a/components/pom.xml +++ b/components/pom.xml @@ -19,6 +19,7 @@ reporters tools user-service + session-service diff --git a/components/session-service/pom.xml b/components/session-service/pom.xml new file mode 100644 index 0000000..45413d6 --- /dev/null +++ b/components/session-service/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + edu.jhuapl.dorset.components + dorset-components + 0.5.0-SNAPSHOT + + session-service + Session Service + http://maven.apache.org + + UTF-8 + + + + com.typesafe + config + 1.0.2 + + + diff --git a/components/session-service/src/main/java/edu/jhuapl/dorset/simplesessionservice/SimpleSessionService.java b/components/session-service/src/main/java/edu/jhuapl/dorset/simplesessionservice/SimpleSessionService.java new file mode 100644 index 0000000..861db60 --- /dev/null +++ b/components/session-service/src/main/java/edu/jhuapl/dorset/simplesessionservice/SimpleSessionService.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017 The Johns Hopkins University Applied Physics Laboratory LLC + * All rights reserved. + * + * 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 edu.jhuapl.dorset.simplesessionservice; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import edu.jhuapl.dorset.sessions.Session; +import edu.jhuapl.dorset.sessions.Session.SessionStatus; +import edu.jhuapl.dorset.sessions.Exchange; +import edu.jhuapl.dorset.sessions.SessionService; + + +public class SimpleSessionService implements SessionService { + + private Map sessions; + + public SimpleSessionService() { + this.sessions = new HashMap(); + } + + @Override + public String create() { + Session session = new Session(); + session.setSessionStatus(SessionStatus.NEW); + this.sessions.put(session.getId(), session); + return session.getId(); + } + + @Override + public void update(String sessionId, Exchange exchange) { + Session currentSession = this.sessions.get(sessionId); + List sessionHistory = currentSession.getExchangeHistory(); + sessionHistory.add(exchange); + + currentSession.setExchangeHistory(sessionHistory); + this.sessions.put(sessionId, currentSession); + } + + @Override + public void delete(String sessionId) { + this.sessions.remove(sessionId); + } + + @Override + public Session getSession(String sessionId) { + return this.sessions.get(sessionId); + } + +} diff --git a/components/session-service/src/test/java/edu/jhuapl/dorset/simplesessionservice/SimpleSessionServiceTest.java b/components/session-service/src/test/java/edu/jhuapl/dorset/simplesessionservice/SimpleSessionServiceTest.java new file mode 100644 index 0000000..9900d9b --- /dev/null +++ b/components/session-service/src/test/java/edu/jhuapl/dorset/simplesessionservice/SimpleSessionServiceTest.java @@ -0,0 +1,16 @@ +package edu.jhuapl.dorset.simplesessionservice; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class SimpleSessionServiceTest { + + @Test + public void testSimpleSessionService() { + + // placeholder for SimpleSessionServiceTest + + } + +} diff --git a/core/src/main/java/edu/jhuapl/dorset/Application.java b/core/src/main/java/edu/jhuapl/dorset/Application.java index 1365273..b1e63ae 100644 --- a/core/src/main/java/edu/jhuapl/dorset/Application.java +++ b/core/src/main/java/edu/jhuapl/dorset/Application.java @@ -32,6 +32,9 @@ import edu.jhuapl.dorset.reporting.Report; import edu.jhuapl.dorset.reporting.Reporter; import edu.jhuapl.dorset.routing.Router; +import edu.jhuapl.dorset.sessions.Session; +import edu.jhuapl.dorset.sessions.Exchange; +import edu.jhuapl.dorset.sessions.SessionService; import edu.jhuapl.dorset.users.User; /** @@ -64,6 +67,7 @@ public class Application { protected Router router; protected Reporter reporter; protected User user; + protected SessionService sessionService; protected List requestFilters; protected List responseFilters; protected List shutdownListeners; @@ -138,6 +142,16 @@ public void addShutdownListener(ShutdownListener listener) { public void setUser(User user) { this.user = user; } + + /** + * Set a SessionService to handle sessions for a Dorset application + * + * @param sessionService a SessionService + * + */ + public void setSessionService(SessionService sessionService) { + this.sessionService = sessionService; + } /** * Process a request @@ -146,6 +160,12 @@ public void setUser(User user) { * @return Response object */ public Response process(Request request) { + + if (this.sessionService != null) { + Response response = this.process(request, this.sessionService); + return response; + } + logger.info("Processing request: " + request.getText()); for (RequestFilter rf : requestFilters) { request = rf.filter(request); @@ -182,6 +202,72 @@ public Response process(Request request) { return response; } + + /** + * Process a request + * + * @param request Request object + * @param sessionService SessionService + * @return Response object + */ + public Response process(Request request, SessionService sessionService) { + + Session currentSession = null; // initialize currentSession + Exchange exchange = new Exchange(); + String sessionId = ""; + if (request.getSession() != null) { // check if it is an ongoing session in the Request + currentSession = request.getSession(); + sessionId = currentSession.getId(); + } else { + sessionId = sessionService.create(); + currentSession = sessionService.getSession(sessionId); + } + + logger.info("Processing request: " + request.getText()); + + exchange.setRequestId(request.getId()); + exchange.setRequest(request); + + for (RequestFilter rf : requestFilters) { + request = rf.filter(request); + exchange.setRequest(request); // replace request with filtered request + } + Response response = new Response(new ResponseStatus( + Code.NO_AVAILABLE_AGENT)); + Report report = new Report(request); + + long startTime = System.nanoTime(); + Agent[] agents = router.route(request); // do we want to include this information in the session as well? + + report.setRouteTime(startTime, System.nanoTime()); + if (agents.length > 0) { + response = new Response(new ResponseStatus( + Code.NO_RESPONSE_FROM_AGENT)); + startTime = System.nanoTime(); + for (Agent agent : agents) { + report.setAgent(agent); + AgentResponse agentResponse = null; + + AgentRequest agentRequest = new AgentRequest(request.getText(), this.user); // Create AgentRequest + agentRequest.setSession(currentSession); // Set the session in the AgentRequest + agentResponse = agent.process(agentRequest); // Process AgentRequest + + if (agentResponse != null) { + + response = new Response(agentResponse); + exchange.setResponse(response); + sessionService.update(sessionId, exchange); + + break; + } + } + report.setAgentTime(startTime, System.nanoTime()); + } + report.setResponse(response); + reporter.store(report); // should we also have an additional store session service? or embed session in report? + + return response; + } /** * Call this when the application is done running diff --git a/core/src/main/java/edu/jhuapl/dorset/Request.java b/core/src/main/java/edu/jhuapl/dorset/Request.java index 31bdc9f..15e6568 100644 --- a/core/src/main/java/edu/jhuapl/dorset/Request.java +++ b/core/src/main/java/edu/jhuapl/dorset/Request.java @@ -18,6 +18,7 @@ import java.util.UUID; +import edu.jhuapl.dorset.sessions.Session; import edu.jhuapl.dorset.users.User; /** @@ -32,6 +33,7 @@ public class Request { private String text; private final String id; private final User user; + private Session session; /** * Create a request @@ -126,5 +128,23 @@ public String getId() { public User getUser() { return user; } - + + /** + * Get the session + * + * @return the user of the request + */ + public Session getSession() { + return session; + } + + /** + * Set the session + * + * @param session the session + */ + public void setSession(Session session) { + this.session = session; + } + } diff --git a/core/src/main/java/edu/jhuapl/dorset/Response.java b/core/src/main/java/edu/jhuapl/dorset/Response.java index e242801..bd4abbb 100644 --- a/core/src/main/java/edu/jhuapl/dorset/Response.java +++ b/core/src/main/java/edu/jhuapl/dorset/Response.java @@ -17,6 +17,8 @@ package edu.jhuapl.dorset; import edu.jhuapl.dorset.agents.AgentResponse; +import edu.jhuapl.dorset.sessions.Session; +import edu.jhuapl.dorset.sessions.Session.SessionStatus; /** * Dorset Response @@ -30,6 +32,8 @@ public class Response { private final String text; private final String payload; private final ResponseStatus status; + private Session session; + private SessionStatus sessionStatus; /** * Create a response @@ -67,6 +71,8 @@ public Response(AgentResponse response) { this.text = response.getText(); this.payload = response.getPayload(); this.status = response.getStatus(); + this.session = response.getSession(); + this.sessionStatus = response.getSessionStatus(); } /** @@ -224,4 +230,40 @@ public static boolean usesPayload(Type type) { return true; } } + + /** + * Get the session + * + * @return the session + */ + public Session getSession() { + return session; + } + + /** + * Set the session + * + * @param session the session + */ + public void setSession(Session session) { + this.session = session; + } + + /** + * Get the session status + * + * @param sessionStatus the session status + */ + public SessionStatus getSessionStatus() { + return sessionStatus; + } + + /** + * Set the session status + * + * @param sessionStatus the session status + */ + public void setSessionStatus(SessionStatus sessionStatus) { + this.sessionStatus = sessionStatus; + } } diff --git a/core/src/main/java/edu/jhuapl/dorset/ResponseStatus.java b/core/src/main/java/edu/jhuapl/dorset/ResponseStatus.java index 8c7b1e8..ffb4a51 100644 --- a/core/src/main/java/edu/jhuapl/dorset/ResponseStatus.java +++ b/core/src/main/java/edu/jhuapl/dorset/ResponseStatus.java @@ -19,6 +19,9 @@ import java.util.HashMap; import java.util.Map; +import edu.jhuapl.dorset.sessions.Session; +import edu.jhuapl.dorset.sessions.Session.SessionStatus; + /** * Response status *

@@ -28,7 +31,9 @@ public class ResponseStatus { private final Code code; private final String message; - + private Session session; + private SessionStatus sessionStatus; + /** * Create a response status based on a code *

@@ -79,6 +84,42 @@ public String getMessage() { return message; } + /** + * Get the session + * + * @return the session + */ + public Session getSession() { + return session; + } + + /** + * Set the session + * + * @param session the session + */ + public void setSession(Session session) { + this.session = session; + } + + /** + * Get the session status + * + * @param sessionStatus the session status + */ + public SessionStatus getSessionStatus() { + return sessionStatus; + } + + /** + * Set the session status + * + * @param sessionStatus the session status + */ + public void setSessionStatus(SessionStatus sessionStatus) { + this.sessionStatus = sessionStatus; + } + @Override public boolean equals(Object obj) { if (obj instanceof ResponseStatus && ((ResponseStatus) obj).getCode() == this.getCode() @@ -109,6 +150,7 @@ public static ResponseStatus createSuccess() { */ public enum Code { SUCCESS(0), + NEEDS_REFINEMENT(1), INTERNAL_ERROR(100), NO_AVAILABLE_AGENT(101), NO_RESPONSE_FROM_AGENT(102), @@ -152,6 +194,7 @@ public static Code fromValue(int value) { private static final Map messageMap = new HashMap(); static { messageMap.put(Code.SUCCESS, "Success"); + messageMap.put(Code.NEEDS_REFINEMENT, "Request needs further information."); messageMap.put(Code.INTERNAL_ERROR, "Something failed with this request."); messageMap.put(Code.NO_AVAILABLE_AGENT, "No agent was available to handle this request."); messageMap.put(Code.NO_RESPONSE_FROM_AGENT, "The agent did not provide a response."); diff --git a/core/src/main/java/edu/jhuapl/dorset/agents/AgentRequest.java b/core/src/main/java/edu/jhuapl/dorset/agents/AgentRequest.java index cda474e..52bfedf 100644 --- a/core/src/main/java/edu/jhuapl/dorset/agents/AgentRequest.java +++ b/core/src/main/java/edu/jhuapl/dorset/agents/AgentRequest.java @@ -17,6 +17,7 @@ package edu.jhuapl.dorset.agents; import edu.jhuapl.dorset.Request; +import edu.jhuapl.dorset.sessions.Session; import edu.jhuapl.dorset.users.User; /** @@ -27,6 +28,7 @@ public class AgentRequest { private String text; private User user; + private Session session; public AgentRequest() {} @@ -98,4 +100,22 @@ public User getUser() { public void setUser(User user) { this.user = user; } + + /** + * Get the session + * + * @return the session of the request + */ + public Session getSession() { + return session; + } + + /** + * Set the session + * + * @param session the session + */ + public void setSession(Session session) { + this.session = session; + } } diff --git a/core/src/main/java/edu/jhuapl/dorset/agents/AgentResponse.java b/core/src/main/java/edu/jhuapl/dorset/agents/AgentResponse.java index 297fe0e..463fd31 100644 --- a/core/src/main/java/edu/jhuapl/dorset/agents/AgentResponse.java +++ b/core/src/main/java/edu/jhuapl/dorset/agents/AgentResponse.java @@ -18,6 +18,8 @@ import edu.jhuapl.dorset.Response; import edu.jhuapl.dorset.ResponseStatus; +import edu.jhuapl.dorset.sessions.Session; +import edu.jhuapl.dorset.sessions.Session.SessionStatus; /** * Response from an agent to a request @@ -29,6 +31,8 @@ public class AgentResponse { private final String text; private final String payload; private final ResponseStatus status; + private Session session; + private SessionStatus sessionStatus; /** * Create an agent response @@ -141,4 +145,40 @@ public boolean isValid() { } return !(isSuccess() && text == null); } + + /** + * Get the session + * + * @return the session of the request + */ + public Session getSession() { + return session; + } + + /** + * Set the session + * + * @param session the session + */ + public void setSession(Session session) { + this.session = session; + } + + /** + * Get the session status + * + * @return sessionStatus the session status + */ + public SessionStatus getSessionStatus() { + return this.sessionStatus; + } + + /** + * Set the session status + * + * @param sessionStatus the session status + */ + public void setSessionStatus(SessionStatus sessionStatus) { + this.sessionStatus = sessionStatus; + } } diff --git a/core/src/main/java/edu/jhuapl/dorset/sessions/Exchange.java b/core/src/main/java/edu/jhuapl/dorset/sessions/Exchange.java new file mode 100644 index 0000000..fc235d3 --- /dev/null +++ b/core/src/main/java/edu/jhuapl/dorset/sessions/Exchange.java @@ -0,0 +1,48 @@ +package edu.jhuapl.dorset.sessions; + +import java.util.Date; + +import edu.jhuapl.dorset.Request; +import edu.jhuapl.dorset.Response; +import edu.jhuapl.dorset.sessions.Session.SessionStatus; + +public class Exchange { + + public String requestId; + public Date timestamp; + public SessionStatus sessionStatus; + public Request request; + public Response response; + + public String getRequestId() { + return requestId; + } + public void setRequestId(String requestId) { + this.requestId = requestId; + } + public Date getTimestamp() { + return timestamp; + } + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + public SessionStatus getSessionStatus() { + return sessionStatus; + } + public void setSessionStatus(SessionStatus sessionStatus) { + this.sessionStatus = sessionStatus; + } + public Request getRequest() { + return request; + } + public void setRequest(Request request) { + this.request = request; + } + public Response getResponse() { + return response; + } + public void setResponse(Response response) { + this.response = response; + } + +} diff --git a/core/src/main/java/edu/jhuapl/dorset/sessions/Session.java b/core/src/main/java/edu/jhuapl/dorset/sessions/Session.java new file mode 100644 index 0000000..ce356c2 --- /dev/null +++ b/core/src/main/java/edu/jhuapl/dorset/sessions/Session.java @@ -0,0 +1,105 @@ +/* + * Copyright 2017 The Johns Hopkins University Applied Physics Laboratory LLC + * All rights reserved. + * + * 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 edu.jhuapl.dorset.sessions; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import edu.jhuapl.dorset.agents.Agent; + +public class Session { + + public final String id; + public Date timestamp; // should have start date, end date, date last updated? + public Agent primaryAgent; + public SessionStatus sessionStatus; + public List exchangeHistory; + + /** + * Create a Session + * + */ + public Session() { + this.id = UUID.randomUUID().toString(); + this.timestamp = new Date(); + this.exchangeHistory = new ArrayList(); + } + + /** + * Create a Session + * + * @param id the session id + * + */ + public Session(String id) { + this.id = id; + this.timestamp = new Date(); + this.exchangeHistory = new ArrayList(); + } + + public String getId() { + return this.id; + } + + public Date getTimestamp() { + return this.timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public Agent getPrimaryAgent() { + return this.primaryAgent; + } + + public void setPrimaryAgent(Agent primaryAgent) { + this.primaryAgent = primaryAgent; + } + + public SessionStatus getSessionStatus() { + return this.sessionStatus; + } + + public void setSessionStatus(SessionStatus status) { + this.sessionStatus = status; + } + + public List getExchangeHistory() { + return this.exchangeHistory; + } + + public void setExchangeHistory(List exchangeHistory) { + this.exchangeHistory = exchangeHistory; + } + + public String sessionToString() { + return "ID: " + this.id + "\nTimestamp: " + this.timestamp.toString() + "\nPrimaryAgent: " + + this.primaryAgent + "\nStatus: " + this.sessionStatus; + } + + public enum SessionStatus { + + NEW, OPEN, CLOSED, TIMED_OUT, ERROR; + + } + +} + diff --git a/core/src/main/java/edu/jhuapl/dorset/sessions/SessionService.java b/core/src/main/java/edu/jhuapl/dorset/sessions/SessionService.java new file mode 100644 index 0000000..e92bbd6 --- /dev/null +++ b/core/src/main/java/edu/jhuapl/dorset/sessions/SessionService.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 The Johns Hopkins University Applied Physics Laboratory LLC + * All rights reserved. + * + * 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 edu.jhuapl.dorset.sessions; + +/** + * Services the session information for a Dorset Application. + *

+ * The session service supports dialog for the Dorset Application and for Agents. + */ +public interface SessionService { + + public String create(); + + public void update(String sessionId, Exchange exchange); + + public void delete(String id); + + public Session getSession(String id); + +} \ No newline at end of file diff --git a/core/src/test/java/edu/jhuapl/dorset/ApplicationTest.java b/core/src/test/java/edu/jhuapl/dorset/ApplicationTest.java index 858c8f6..d011ce6 100644 --- a/core/src/test/java/edu/jhuapl/dorset/ApplicationTest.java +++ b/core/src/test/java/edu/jhuapl/dorset/ApplicationTest.java @@ -30,6 +30,9 @@ import edu.jhuapl.dorset.filters.WakeupRequestFilter; import edu.jhuapl.dorset.routing.Router; import edu.jhuapl.dorset.routing.SingleAgentRouter; +import edu.jhuapl.dorset.sessions.Session; +import edu.jhuapl.dorset.sessions.SessionService; +import edu.jhuapl.dorset.sessions.Session.SessionStatus; public class ApplicationTest { @Test @@ -100,7 +103,62 @@ public AgentResponse answer(InvocationOnMock invocation) { assertEquals("test", response.getText()); } + + @Test + public void testAddingSessionsNewSession() { + Agent agent = mock(Agent.class); + when(agent.process((AgentRequest) anyObject())).thenAnswer(new Answer() { + @Override + public AgentResponse answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + AgentResponse agentResponse = new AgentResponse(((AgentRequest) args[0]).getText()); + agentResponse.setSessionStatus(SessionStatus.CLOSED); + return agentResponse; + } + }); + Router router = new SingleAgentRouter(agent); + Application app = new Application(router); + SessionService sessionService = mock(SessionService.class); + app.setSessionService(sessionService); + + Request request = new Request("test"); + Response response = app.process(request); + + assertEquals("test", response.getText()); + assertEquals(SessionStatus.CLOSED, response.getSessionStatus()); + + } + + @Test + public void testAddingSessionsExistingSession() { + Agent agent = mock(Agent.class); + when(agent.process((AgentRequest) anyObject())).thenAnswer(new Answer() { + @Override + public AgentResponse answer(InvocationOnMock invocation) { + Object[] args = invocation.getArguments(); + AgentResponse agentResponse = new AgentResponse(((AgentRequest) args[0]).getText()); + agentResponse.setSessionStatus(SessionStatus.OPEN); + return agentResponse; + } + }); + + Router router = new SingleAgentRouter(agent); + Application app = new Application(router); + + SessionService sessionService = mock(SessionService.class); + + Session session = new Session(); + Request request = new Request("test"); + request.setSession(session); + + Response response = app.process(request, sessionService); + + assertEquals("test", response.getText()); + assertEquals(SessionStatus.OPEN, response.getSessionStatus()); + + } + @Test public void testShutdown() { Router router = mock(Router.class);