package edu.stanford.nlp.sempre; import com.google.common.collect.Lists; import fig.basic.*; import fig.html.HtmlElement; import fig.html.HtmlUtils; import java.net.InetSocketAddress; import java.net.URI; import java.net.URLDecoder; import java.net.HttpCookie; import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.Headers; import java.io.*; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; import java.math.BigInteger; import java.security.SecureRandom; import static fig.basic.LogInfo.logs; final class SecureIdentifiers { private SecureIdentifiers() { } private static SecureRandom random = new SecureRandom(); public static String getId() { return new BigInteger(130, random).toString(32); } } /** * This class implements a simple HTTP server which provides a web interface * into SEMPRE just like Master.runInteractivePrompt() exposes a command-line * tool. Most of the work is dispatched to Master.processLine(). * Cookies are used to store the session ID. * * @author Percy Liang */ public class Server { public static class Options { @Option public int port = 8400; @Option public int numThreads = 4; @Option public String title = "SEMPRE Demo"; @Option public String headerPath; @Option public String basePath = "demo-www"; @Option public int verbose = 1; @Option public int htmlVerbose = 1; } public static Options opts = new Options(); Master master; public static final HtmlUtils H = new HtmlUtils(); class Handler implements HttpHandler { public void handle(HttpExchange exchange) { try { new ExchangeState(exchange); } catch (Exception e) { e.printStackTrace(); } } } class ExchangeState { // Input HttpExchange exchange; Map<String, String> reqParams = new HashMap<>(); String remoteHost; // For header HttpCookie cookie; boolean isNewSession; String format; boolean jsonFormat() { return format.equals("json"); } // For writing main content public ExchangeState(HttpExchange exchange) throws IOException { this.exchange = exchange; URI uri = exchange.getRequestURI(); this.remoteHost = exchange.getRemoteAddress().getHostName(); // Don't use uri.getQuery: it can't distinguish between '+' and '-' String[] tokens = uri.toString().split("\\?"); if (tokens.length == 2) { for (String s : tokens[1].split("&")) { String[] kv = s.split("=", 2); try { String key = URLDecoder.decode(kv[0], "UTF-8"); String value = URLDecoder.decode(kv[1], "UTF-8"); logs("%s => %s", key, value); reqParams.put(key, value); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } } this.format = MapUtils.get(reqParams, "format", "html"); String cookieStr = exchange.getRequestHeaders().getFirst("Cookie"); if (cookieStr != null) { // Cookie already exists cookie = HttpCookie.parse(cookieStr).get(0); isNewSession = false; } else { if (!jsonFormat()) { cookie = new HttpCookie("sessionId", SecureIdentifiers.getId()); } else { cookie = null; } isNewSession = true; // Create a new cookie } String sessionId = null; if (cookie != null) sessionId = cookie.getValue(); if (opts.verbose >= 2) LogInfo.logs("GET %s from %s (%ssessionId=%s)", uri, remoteHost, isNewSession ? "new " : "", sessionId); String uriPath = uri.getPath(); if (uriPath.equals("/")) uriPath += "index.html"; if (uriPath.equals("/sempre")) { handleQuery(sessionId); } else { getFile(opts.basePath + uriPath); } exchange.close(); } String getMimeType(String path) { String[] tokens = path.split("\\."); String ext = tokens[tokens.length - 1]; if (ext.equals("html")) return "text/html"; if (ext.equals("css")) return "text/css"; if (ext.equals("jpeg")) return "image/jpeg"; if (ext.equals("gif")) return "image/gif"; return "text/plain"; } void setHeaders(String mimeType) throws IOException { Headers headers = exchange.getResponseHeaders(); headers.set("Content-Type", mimeType); headers.set("Access-Control-Allow-Origin", "*"); if (isNewSession && cookie != null) headers.set("Set-Cookie", cookie.toString()); exchange.sendResponseHeaders(200, 0); } private HtmlElement makeInputBox(String line, String action) { return H.div().child( H.form().action(action) .child(H.text(line == null ? "" : line).cls("question").autofocus().size(50).name("q")) .child(H.button("Go").cls("ask")) .end()); } private HtmlElement makeTooltip(HtmlElement main, HtmlElement aux) { return H.a().cls("info").child(main).child(H.span().cls("tooltip").child(aux)); } private HtmlElement makeTooltip(HtmlElement main, HtmlElement aux, String link) { return H.a().href(link).cls("info").child(main).child(H.span().cls("tooltip").child(aux)); } public final String freebaseWebsite = "http://www.freebase.com/"; public String id2website(String id) { assert id.startsWith("fb:") : id; return freebaseWebsite + id.substring(3).replaceAll("\\.", "/"); } HtmlElement valueToElem(Value value) { if (value == null) return H.span(); if (value instanceof NameValue) { NameValue nameValue = (NameValue) value; return H.a().href(id2website(nameValue.id)).child(nameValue.description == null ? nameValue.id : nameValue.description); } else if (value instanceof NumberValue) { NumberValue numberValue = (NumberValue) value; return H.span().child(Fmt.D(numberValue.value) + (numberValue.unit.equals(NumberValue.unitless) ? "" : " " + numberValue.unit)); } else if (value instanceof UriValue) { UriValue uriValue = (UriValue) value; return H.a().href(uriValue.value).child(uriValue.value); } else if (value instanceof DateValue) { DateValue dateValue = (DateValue) value; return H.span().child(dateValue.year + (dateValue.month == -1 ? "" : "-" + dateValue.month + (dateValue.day == -1 ? "" : "-" + dateValue.day))); } else if (value instanceof StringValue) { return H.span().child(((StringValue) value).value); } else if (value instanceof TableValue) { HtmlElement table = H.table().cls("valueTable"); HtmlElement header = H.tr(); boolean first = true; for (String item : ((TableValue) value).header) { if (!first) header.child(H.td("   ")); first = false; header.child(H.td(H.b(item))); } table.child(header); for (List<Value> rowValues : ((TableValue) value).rows) { HtmlElement row = H.tr(); first = true; for (Value x : rowValues) { // TODO(pliang): add horizontal spacing only using CSS if (!first) row.child(H.td("   ")); first = false; row.child(H.td(valueToElem(x))); } table.child(row); } return table; } else { // Default rendering return H.span().child(value.toString()); } } private HtmlElement makeAnswerBox(Master.Response response, String uri) { HtmlElement answer; if (response.getExample().getPredDerivations().size() == 0) { answer = H.span().child("(none)"); } else { answer = valueToElem(response.getDerivation().getValue()); } return H.table().child( H.tr() .child( H.td( makeTooltip( H.span().cls("correctButton").child("[Correct]"), H.div().cls("bubble").child("If this answer is correct, click to add as a new training example!"), uri + "&accept=" + response.getCandidateIndex()))) .child(H.td(H.span().cls("answer").child(answer))) .end()); } private HtmlElement makeGroup(List<HtmlElement> items) { HtmlElement table = H.table().cls("groupResponse"); for (HtmlElement item : items) table.child(H.tr().child(H.td(item))); return table; } HtmlElement makeDetails(Master.Response response, String uri) { Example ex = response.getExample(); List<HtmlElement> items = new ArrayList<HtmlElement>(); if (opts.htmlVerbose >= 1) items.add(makeLexical(ex)); if (ex.getPredDerivations().size() > 0) { if (opts.htmlVerbose >= 1) { items.add(makeDerivation(ex, response.getDerivation(), true)); items.add(makeFeatures(response.getDerivation(), false)); } items.add(makeCandidates(ex, uri)); } return H.div().cls("details").child(makeGroup(items)); } HtmlElement makeDerivation(Example ex, Derivation deriv, boolean moreInfo) { HtmlElement table = H.table(); // Show the derivation table.child(H.tr().child(H.td(makeDerivationHelper(ex, deriv, "", moreInfo)))); String header = "Derivation"; return H.div() .child(H.span().cls("listHeader").child(header)) .child(table); } HtmlElement makeDerivationHelper(Example ex, Derivation deriv, String indent, boolean moreInfo) { // TODO(pliang): make this prettier HtmlElement cat; if (moreInfo) { HtmlElement tooltip = H.div(); tooltip.child(H.span(deriv.rule.toString()).cls("nowrap")); tooltip.child(makeFeatures(deriv, true)); cat = makeTooltip(H.span(deriv.cat), tooltip); } else { cat = H.span(deriv.cat); } String description = cat + "[ " + H.span().child(ex.phraseString(deriv.start, deriv.end)).cls("word") + "]" + " → " + deriv.formula; HtmlElement node = H.div().child(indent + description); for (Derivation child : deriv.children) node.child(makeDerivationHelper(ex, child, indent + "    ", moreInfo)); return node; } HtmlElement makeFeatures(Derivation deriv, boolean local) { HtmlElement table = H.table(); Params params = master.getParams(); Map<String, Double> features = new HashMap<String, Double>(); if (local) deriv.incrementLocalFeatureVector(1, features); else deriv.incrementAllFeatureVector(1, features); List<Map.Entry<String, Double>> entries = Lists.newArrayList(); double sumValue = 0; for (Map.Entry<String, Double> entry : features.entrySet()) { String feature = entry.getKey(); if (entry.getValue() == 0) continue; double value = entry.getValue() * params.getWeight(feature); sumValue += value; entries.add(new java.util.AbstractMap.SimpleEntry<String, Double>(feature, value)); } Collections.sort(entries, new ValueComparator<String, Double>(false)); table.child( H.tr() .child(H.td(H.b("Feature"))) .child(H.td(H.b("Value"))) .child(H.td(H.b("Weight")))); for (Map.Entry<String, Double> entry : entries) { String feature = entry.getKey(); double value = entry.getValue(); double weight = params.getWeight(feature); table.child( H.tr() .child(H.td(feature)) .child(H.td(Fmt.D(MapUtils.getDouble(features, feature, 0)))) .child(H.td(Fmt.D(weight)))); } String header; if (local) { double localScore = deriv.localScore(params); double score = deriv.getScore(); if (deriv.children == null) header = String.format("Local features (score = %s)", Fmt.D(score)); else header = String.format("Local features (score = %s + %s = %s)", Fmt.D(score - localScore), Fmt.D(localScore), Fmt.D(score)); } else { header = String.format("All features (score=%s, prob=%s)", Fmt.D(deriv.getScore()), Fmt.D(deriv.getProb())); } return H.div() .child(H.span().cls("listHeader").child(header)) .child(table); } HtmlElement linkSelect(int index, String uri, String str) { return H.a().href(uri + "&select=" + index).child(str); } private HtmlElement makeCandidates(Example ex, String uri) { HtmlElement table = H.table().cls("candidateTable"); HtmlElement header = H.tr() .child(H.td(H.b("Rank"))) .child(H.td(H.b("Score"))) .child(H.td(H.b("Answer"))); if (opts.htmlVerbose >= 1) header.child(H.td(H.b("Formula"))); table.child(header); for (int i = 0; i < ex.getPredDerivations().size(); i++) { Derivation deriv = ex.getPredDerivations().get(i); HtmlElement correct = makeTooltip( H.span().cls("correctButton").child("[Correct]"), H.div().cls("bubble").child("If this answer is correct, click to add as a new training example!"), uri + "&accept=" + i); String value = shorten(deriv.getValue() == null ? "" : deriv.getValue().toString(), 200); HtmlElement formula = makeTooltip( H.span(deriv.getFormula().toString()), H.div().cls("nowrap").child(makeDerivation(ex, deriv, false)), uri + "&select=" + i); HtmlElement row = H.tr() .child(H.td(linkSelect(i, uri, i + " " + correct)).cls("nowrap")) .child(H.td(Fmt.D(deriv.getScore()))) .child(H.td(value)).style("width:250px"); if (opts.htmlVerbose >= 1) row.child(H.td(formula)); table.child(row); } return H.div() .child(H.span().cls("listHeader").child("Candidates")) .child(table); } private String shorten(String s, int n) { if (s.length() <= n) return s; return s.substring(0, n / 2) + "..." + s.substring(s.length() - n / 2); } private void markLexical(Derivation deriv, CandidatePredicates[] predicates) { // TODO(pliang): generalize this to the case where the formula is a // NameFormula but the child is a StringFormula? if (deriv.getRule() != null && deriv.getRule().getSem() != null && deriv.getRule().getSem().getClass().getSimpleName().equals("LexiconFn")) predicates[deriv.getStart()].add(deriv.getFormula(), deriv.getEnd() - deriv.getStart(), deriv.getScore()); for (Derivation child : deriv.getChildren()) markLexical(child, predicates); } class CandidatePredicates { // Parallel arrays List<Formula> predicates = new ArrayList<Formula>(); List<Integer> spanLengths = new ArrayList<Integer>(); List<Double> scores = new ArrayList<Double>(); void add(Formula formula, int spanLength, double score) { predicates.add(formula); spanLengths.add(spanLength); scores.add(score); } int size() { return predicates.size(); } String formatPredicate(int i) { return predicates.get(i).toString() + (spanLengths.get(i) == 1 ? "" : " [" + spanLengths.get(i) + "]"); } } // Move to fig double[] toDoubleArray(List<Double> l) { double[] a = new double[l.size()]; for (int i = 0; i < l.size(); i++) a[i] = l.get(i); return a; } HtmlElement makeLexical(Example ex) { HtmlElement predicatesElem = H.tr(); HtmlElement tokensElem = H.tr(); // Mark all the predicates used in any derivation on the beam. // Note: this is not all possible. CandidatePredicates[] predicates = new CandidatePredicates[ex.getTokens().size()]; for (int i = 0; i < ex.getTokens().size(); i++) predicates[i] = new CandidatePredicates(); for (Derivation deriv : ex.getPredDerivations()) markLexical(deriv, predicates); // Build up |predicatesElem| and |tokensElem| for (int i = 0; i < ex.getTokens().size(); i++) { tokensElem.child( H.td( makeTooltip( H.span().cls("word").child(ex.getTokens().get(i)), H.span().cls("tag").child("POS: " + ex.languageInfo.posTags.get(i)), ""))); if (predicates[i].size() == 0) { predicatesElem.child(H.td("")); } else { // Show possible predicates for a word HtmlElement pe = H.table().cls("predInfo"); int[] perm = ListUtils.sortedIndices(toDoubleArray(predicates[i].scores), true); Set<String> formulaSet = new HashSet<String>(); for (int j : perm) { String formula = predicates[i].formatPredicate(j); if (formulaSet.contains(formula)) continue; // Dedup formulaSet.add(formula); double score = predicates[i].scores.get(j); pe.child(H.tr().child(H.td(formula)).child(H.td(Fmt.D(score)))); } predicatesElem.child(H.td(makeTooltip(H.span().child(predicates[i].formatPredicate(perm[0])), pe, ""))); } } return H.div().cls("lexicalResponse") .child(H.span().cls("listHeader").child("Lexical Triggers")) .child(H.table().child(predicatesElem).child(tokensElem)); } String makeJson(Master.Response response) { Map<String, Object> json = new HashMap<String, Object>(); List<Object> items = new ArrayList<Object>(); json.put("candidates", items); for (Derivation deriv : response.getExample().getPredDerivations()) { Map<String, Object> item = new HashMap<String, Object>(); Value value = deriv.getValue(); if (value instanceof UriValue) { item.put("url", ((UriValue) value).value); } else if (value instanceof TableValue) { TableValue tableValue = (TableValue) value; item.put("header", tableValue.header); List<List<String>> rowsObj = new ArrayList<List<String>>(); item.put("rows", rowsObj); for (List<Value> row : tableValue.rows) { List<String> rowObj = new ArrayList<String>(); for (Value v : row) rowObj.add(v.toString()); rowsObj.add(rowObj); } } else { item.put("value", value.toString()); } item.put("score", deriv.score); item.put("prob", deriv.prob); items.add(item); } return Json.writeValueAsStringHard(json); } // Catch exception if any. Master.Response processQuery(Session session, String query) { try { return master.processQuery(session, query); } catch (Throwable t) { t.printStackTrace(); return null; } } // If query is not already the last query, make it the last query. boolean ensureQueryIsLast(Session session, String query) { if (query != null && !query.equals(session.getLastQuery())) { Master.Response response = processQuery(session, query); if (response == null) return false; } return true; } void handleQuery(String sessionId) throws IOException { String query = reqParams.get("q"); // If JSON, don't store cookies. Session session = master.getSession(sessionId); session.remoteHost = remoteHost; session.format = format; if (query == null) query = session.getLastQuery(); if (query == null) query = ""; logs("Server.handleQuery %s: %s", session.id, query); // Print header if (jsonFormat()) setHeaders("application/json"); else setHeaders("text/html"); PrintWriter out = new PrintWriter(new OutputStreamWriter(exchange.getResponseBody())); if (!jsonFormat()) { out.println("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">"); out.println(H.html().open()); out.println( H.head() .child(H.title(opts.title)) .child(H.link().rel("stylesheet").type("text/css").href("main.css")) .child(H.script().src("main.js")) .end()); out.println(H.body().open()); if (opts.headerPath != null) { for (String line : IOUtils.readLinesHard(opts.headerPath)) out.println(line); } } String uri = exchange.getRequestURI().toString(); // Encode the URL parameters into the freeform text. // A bit backwards, but keeps uniformity. String select = reqParams.get("select"); if (select != null) { if (ensureQueryIsLast(session, query)) query = LispTree.proto.newList("select", select).toString(); else query = null; } String accept = reqParams.get("accept"); if (accept != null) { if (ensureQueryIsLast(session, query)) query = LispTree.proto.newList("accept", accept).toString(); else query = null; } // Handle the request Master.Response masterResponse = null; if (query != null) masterResponse = processQuery(session, query); // Print history of exchanges if (session.context.exchanges.size() > 0 && !jsonFormat()) { HtmlElement context = H.table().cls("context"); for (ContextValue.Exchange e : session.context.exchanges) { HtmlElement row = H.tr().child(H.td(H.span().cls("word").child(e.utterance))); row.child(H.td(H.span("    "))).child(H.td(e.value.toString())); if (opts.htmlVerbose >= 1) row.child(H.td(H.span("    "))).child(H.td(e.formula.toString())); context.child(row); } out.println(context.toString()); } // Print input box for new utterance if (!jsonFormat()) { String defaultQuery = query != null ? query : session.getLastQuery(); out.println(makeInputBox(defaultQuery, uri).toString()); } if (masterResponse != null) { // Render answer Example ex = masterResponse.getExample(); if (ex != null) { if (!jsonFormat()) { out.println(makeAnswerBox(masterResponse, uri).toString()); out.println(makeDetails(masterResponse, uri).toString()); } else { out.println(makeJson(masterResponse)); } } if (!jsonFormat() && opts.htmlVerbose >= 1) { // Write response to user out.println(H.elem("pre").open()); for (String outLine : masterResponse.getLines()) out.println(outLine); out.println(H.elem("pre").close()); } } else { if (query != null && !jsonFormat()) out.println(H.span("Internal error!").cls("error")); } if (!jsonFormat()) { out.println(H.body().close()); out.println(H.html().close()); } out.close(); } void getResults() throws IOException { setHeaders("application/json"); Map<String, String> map = new HashMap<>(); map.put("a", "3"); map.put("b", "4"); PrintWriter writer = new PrintWriter(new OutputStreamWriter(exchange.getResponseBody())); writer.println(Json.writeValueAsStringHard(map)); writer.close(); } void getFile(String path) throws IOException { if (!new File(path).exists()) { LogInfo.logs("File doesn't exist: %s", path); exchange.sendResponseHeaders(404, 0); // File not found return; } setHeaders(getMimeType(path)); if (opts.verbose >= 2) LogInfo.logs("Sending %s", path); OutputStream out = new BufferedOutputStream(exchange.getResponseBody()); InputStream in = new FileInputStream(path); IOUtils.copy(in, out); } } public Server(Master master) { this.master = master; } void run() { try { String hostname = fig.basic.SysInfoUtils.getHostName(); HttpServer server = HttpServer.create(new InetSocketAddress(opts.port), 10); ExecutorService pool = Executors.newFixedThreadPool(opts.numThreads); server.createContext("/", new Handler()); server.setExecutor(pool); server.start(); LogInfo.logs("Server started at http://%s:%s/sempre", hostname, opts.port); LogInfo.log("Press Ctrl-D to terminate."); while (LogInfo.stdin.readLine() != null) { } LogInfo.log("Shutting down server..."); server.stop(0); LogInfo.log("Shutting down executor pool..."); pool.shutdown(); } catch (IOException e) { throw new RuntimeException(e); } } }