package edu.stanford.nlp.sempre;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import fig.basic.*;
import jline.console.ConsoleReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.util.*;
/**
* A Master manages multiple sessions. Currently, they all share the same model,
* but they need not in the future.
*/
public class Master {
public static class Options {
@Option(gloss = "Execute these commands before starting")
public List<String> scriptPaths = Lists.newArrayList();
@Option(gloss = "Execute these commands before starting (after scriptPaths)")
public List<String> commands = Lists.newArrayList();
@Option(gloss = "Write a log of this session to this path")
public String logPath;
@Option(gloss = "Print help on startup")
public boolean printHelp = true;
@Option(gloss = "Number of exchanges to keep in the context")
public int contextMaxExchanges = 0;
@Option(gloss = "Online update weights on new examples.")
public boolean onlineLearnExamples = true;
@Option(gloss = "Write out new examples to this directory")
public String newExamplesPath;
@Option(gloss = "Write out new parameters to this directory")
public String newParamsPath;
@Option(gloss = "Write out new grammar rules")
public String newGrammarPath;
}
public static Options opts = new Options();
public class Response {
// Example that was parsed, if any.
public Example ex;
// Which derivation we're selecting to show
int candidateIndex = -1;
// Detailed information
public Map<String, Object> stats = new LinkedHashMap<>();
public List<String> lines = new ArrayList<>();
public String getFormulaAnswer() {
if (ex.getPredDerivations().size() == 0)
return "(no answer)";
else if (candidateIndex == -1)
return "(not selected)";
else {
Derivation deriv = getDerivation();
return deriv.getFormula() + " => " + deriv.getValue();
}
}
public String getAnswer() {
if (ex.getPredDerivations().size() == 0)
return "(no answer)";
else if (candidateIndex == -1)
return "(not selected)";
else {
Derivation deriv = getDerivation();
deriv.ensureExecuted(builder.executor, ex.context);
return deriv.getValue().toString();
}
}
public List<String> getLines() { return lines; }
public Example getExample() { return ex; }
public int getCandidateIndex() { return candidateIndex; }
public Derivation getDerivation() {
return ex.getPredDerivations().get(candidateIndex);
}
}
protected Builder builder;
protected Learner learner;
protected HashMap<String, Session> sessions = new LinkedHashMap<>();
public Master(Builder builder) {
this.builder = builder;
this.learner = new Learner(builder.parser, builder.params, new Dataset());
}
public Params getParams() { return builder.params; }
// Return the unique session identified by session id |id|.
// Create a new session if one doesn't exist.
public Session getSession(String id) {
Session session = sessions.get(id);
if (session == null) {
session = new Session(id);
for (String path : opts.scriptPaths)
processScript(session, path);
for (String command : opts.commands)
processQuery(session, command);
if (id != null)
sessions.put(id, session);
}
return session;
}
protected void printHelp() {
LogInfo.log("Enter an utterance to parse or one of the following commands:");
LogInfo.log(" (help): show this help message");
LogInfo.log(" (status): prints out status of the system");
LogInfo.log(" (get |option|): get a command-line option (e.g., (get Parser.verbose))");
LogInfo.log(" (set |option| |value|): set a command-line option (e.g., (set Parser.verbose 5))");
LogInfo.log(" (reload): reload the grammar/parameters");
LogInfo.log(" (grammar): prints out the grammar");
LogInfo.log(" (params [|file|]): dumps all the model parameters");
LogInfo.log(" (select |candidate index|): show information about the |index|-th candidate of the last utterance.");
LogInfo.log(" (accept |candidate index|): record the |index|-th candidate as the correct answer for the last utterance.");
LogInfo.log(" (answer |answer|): record |answer| as the correct answer for the last utterance (e.g., (answer (list (number 3)))).");
LogInfo.log(" (rule |lhs| (|rhs_1| ... |rhs_k|) |sem|): adds a rule to the grammar (e.g., (rule $Number ($TOKEN) (NumberFn)))");
LogInfo.log(" (type |logical form|): perform type inference (e.g., (type (number 3)))");
LogInfo.log(" (execute |logical form|): execute the logical form (e.g., (execute (call + (number 3) (number 4))))");
LogInfo.log(" (def |key| |value|): define a macro to replace |key| with |value| in all commands (e.g., (def type fb:type.object type)))");
LogInfo.log(" (context [(user |user|) (date |date|) (exchange |exchange|) (graph |graph|)]): prints out or set the context");
LogInfo.log("Press Ctrl-D to exit.");
}
public void runServer() {
Server server = new Server(this);
server.run();;
}
public void runInteractivePrompt() {
Session session = getSession("stdin");
if (opts.printHelp)
printHelp();
try {
ConsoleReader reader = new ConsoleReader();
reader.setPrompt("> ");
String line;
while ((line = reader.readLine()) != null) {
int indent = LogInfo.getIndLevel();
try {
processQuery(session, line);
} catch (Throwable t) {
while (LogInfo.getIndLevel() > indent)
LogInfo.end_track();
t.printStackTrace();
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Read LispTrees from |scriptPath| and process each of them.
public void processScript(Session session, String scriptPath) {
Iterator<LispTree> it = LispTree.proto.parseFromFile(scriptPath);
while (it.hasNext()) {
LispTree tree = it.next();
processQuery(session, tree.toString());
}
}
// Process user's input |line|
// Currently, synchronize a very crude level.
// In the future, refine this.
// Currently need the synchronization because of writing to stdout.
public synchronized Response processQuery(Session session, String line) {
line = line.trim();
Response response = new Response();
// Capture log output and put it into response.
// Hack: modifying a static variable to capture the logging.
// Make sure we're synchronized!
StringWriter stringOut = new StringWriter();
LogInfo.setFileOut(new PrintWriter(stringOut));
if (line.startsWith("("))
handleCommand(session, line, response);
else
handleUtterance(session, line, response);
// Clean up
for (String outLine : stringOut.toString().split("\n"))
response.lines.add(outLine);
LogInfo.setFileOut(null);
// Log interaction to disk
if (!Strings.isNullOrEmpty(opts.logPath)) {
PrintWriter out = IOUtils.openOutAppendHard(opts.logPath);
out.println(
Joiner.on("\t").join(
Lists.newArrayList(
"date=" + new Date().toString(),
"sessionId=" + session.id,
"remote=" + session.remoteHost,
"format=" + session.format,
"query=" + line,
"response=" + summaryString(response))));
out.close();
}
return response;
}
String summaryString(Response response) {
if (response.getExample() != null)
return response.getFormulaAnswer();
if (response.getLines().size() > 0)
return response.getLines().get(0);
return null;
}
private void handleUtterance(Session session, String query, Response response) {
session.updateContext();
// Create example
Example.Builder b = new Example.Builder();
b.setId("session:" + session.id);
b.setUtterance(query);
b.setContext(session.context);
Example ex = b.createExample();
ex.preprocess();
// Parse!
builder.parser.parse(builder.params, ex, false);
response.ex = ex;
ex.logWithoutContext();
if (ex.predDerivations.size() > 0) {
response.candidateIndex = 0;
printDerivation(response.getDerivation());
}
session.updateContext(ex, opts.contextMaxExchanges);
}
private void printDerivation(Derivation deriv) {
// Print features
HashMap<String, Double> featureVector = new HashMap<>();
deriv.incrementAllFeatureVector(1, featureVector);
FeatureVector.logFeatureWeights("Pred", featureVector, builder.params);
// Print choices
Map<String, Integer> choices = new LinkedHashMap<>();
deriv.incrementAllChoices(1, choices);
FeatureVector.logChoices("Pred", choices);
// Print denotation
LogInfo.begin_track("Top formula");
LogInfo.logs("%s", deriv.formula);
LogInfo.end_track();
if (deriv.value != null) {
LogInfo.begin_track("Top value");
deriv.value.log();
LogInfo.end_track();
}
}
private void handleCommand(Session session, String line, Response response) {
LispTree tree = LispTree.proto.parseFromString(line);
tree = builder.grammar.applyMacros(tree);
String command = tree.child(0).value;
if (command == null || command.equals("help")) {
printHelp();
} else if (command.equals("status")) {
LogInfo.begin_track("%d sessions", sessions.size());
for (Session otherSession : sessions.values())
LogInfo.log(otherSession + (session == otherSession ? " *" : ""));
LogInfo.end_track();
StopWatchSet.logStats();
} else if (command.equals("reload")) {
builder.build();
} else if (command.equals("grammar")) {
for (Rule rule : builder.grammar.rules)
LogInfo.logs("%s", rule.toLispTree());
} else if (command.equals("params")) {
if (tree.children.size() == 1) {
builder.params.write(LogInfo.stdout);
if (LogInfo.getFileOut() != null)
builder.params.write(LogInfo.getFileOut());
} else {
builder.params.write(tree.child(1).value);
}
} else if (command.equals("get")) {
if (tree.children.size() != 2) {
LogInfo.log("Invalid usage: (get |option|)");
return;
}
String option = tree.child(1).value;
LogInfo.logs("%s", getOptionsParser().getValue(option));
} else if (command.equals("set")) {
if (tree.children.size() != 3) {
LogInfo.log("Invalid usage: (set |option| |value|)");
return;
}
String option = tree.child(1).value;
String value = tree.child(2).value;
if (!getOptionsParser().parse(new String[] {"-" + option, value}))
LogInfo.log("Unknown option: " + option);
} else if (command.equals("select") || command.equals("accept") ||
command.equals("s") || command.equals("a")) {
// Select an answer
if (tree.children.size() != 2) {
LogInfo.logs("Invalid usage: (%s |candidate index|)", command);
return;
}
Example ex = session.getLastExample();
if (ex == null) {
LogInfo.log("No examples - please enter a query first.");
return;
}
int index = Integer.parseInt(tree.child(1).value);
if (index < 0 || index >= ex.predDerivations.size()) {
LogInfo.log("Candidate index out of range: " + index);
return;
}
response.ex = ex;
response.candidateIndex = index;
session.updateContextWithNewAnswer(ex, response.getDerivation());
printDerivation(response.getDerivation());
// Add a training example. While the user selects a particular derivation, there are three ways to interpret this signal:
// 1. This is the correct derivation (Derivation).
// 2. This is the correct logical form (Formula).
// 3. This is the correct denotation (Value).
// Currently:
// - Parameters based on the denotation.
// - Grammar rules are induced based on the denotation.
// We always save the logical form and the denotation (but not the entire
// derivation) in the example.
if (command.equals("accept") || command.equals("a")) {
ex.setTargetFormula(response.getDerivation().getFormula());
ex.setTargetValue(response.getDerivation().getValue());
ex.setContext(session.getContextExcludingLast());
addNewExample(ex);
}
} else if (command.equals("answer")) {
if (tree.children.size() != 2) {
LogInfo.log("Missing answer.");
}
// Set the target value.
Example ex = session.getLastExample();
if (ex == null) {
LogInfo.log("Please enter a query first.");
return;
}
ex.setTargetValue(Values.fromLispTree(tree.child(1)));
addNewExample(ex);
} else if (command.equals("rule")) {
int n = builder.grammar.rules.size();
builder.grammar.addStatement(tree.toString());
for (int i = n; i < builder.grammar.rules.size(); i++)
LogInfo.logs("Added %s", builder.grammar.rules.get(i));
// Need to update the parser given that the grammar has changed.
builder.parser = null;
builder.buildUnspecified();
} else if (command.equals("type")) {
LogInfo.logs("%s", TypeInference.inferType(Formulas.fromLispTree(tree.child(1))));
} else if (command.equals("execute")) {
Example ex = session.getLastExample();
ContextValue context = (ex != null ? ex.context : session.context);
Executor.Response execResponse = builder.executor.execute(Formulas.fromLispTree(tree.child(1)), context);
LogInfo.logs("%s", execResponse.value);
} else if (command.equals("def")) {
builder.grammar.interpretMacroDef(tree);
} else if (command.equals("context")) {
if (tree.children.size() == 1) {
LogInfo.logs("%s", session.context);
} else {
session.context = new ContextValue(tree);
}
} else if (command.equals("loadgraph")) {
if (tree.children.size() != 2 || !tree.child(1).isLeaf())
throw new RuntimeException("Invalid argument: argument should be a file path");
KnowledgeGraph graph = NaiveKnowledgeGraph.fromFile(tree.child(1).value);
session.context = new ContextValue(session.context.user, session.context.date,
session.context.exchanges, graph);
}
else {
LogInfo.log("Invalid command: " + tree);
}
}
void addNewExample(Example origEx) {
// Create the new example, but only add relevant information.
Example ex = new Example.Builder()
.setId(origEx.id)
.setUtterance(origEx.utterance)
.setContext(origEx.context)
.setTargetFormula(origEx.targetFormula)
.setTargetValue(origEx.targetValue)
.createExample();
if (!Strings.isNullOrEmpty(opts.newExamplesPath)) {
LogInfo.begin_track("Adding new example");
Dataset.appendExampleToFile(opts.newExamplesPath, ex);
LogInfo.end_track();
}
if (opts.onlineLearnExamples) {
LogInfo.begin_track("Updating parameters");
learner.onlineLearnExample(origEx);
if (!Strings.isNullOrEmpty(opts.newParamsPath))
builder.params.write(opts.newParamsPath);
LogInfo.end_track();
}
}
public static OptionsParser getOptionsParser() {
OptionsParser parser = new OptionsParser();
// Dynamically figure out which options we need to load
// To specify this:
// java -Dmodules=core,freebase
List<String> modules = Arrays.asList(System.getProperty("modules", "core").split(","));
// All options are assumed to be of the form <class>opts.
// Read the module-classes.txt file, which specifies which classes are
// associated with each module.
List<Object> args = new ArrayList<Object>();
for (String line : IOUtils.readLinesHard("module-classes.txt")) {
// Example: core edu.stanford.nlp.sempre.Grammar
String[] tokens = line.split(" ");
if (tokens.length != 2) throw new RuntimeException("Invalid: " + line);
String module = tokens[0];
String className = tokens[1];
if (!modules.contains(tokens[0])) continue;
// Group (e.g., Grammar)
String[] classNameTokens = className.split("\\.");
String group = classNameTokens[classNameTokens.length - 1];
// Object (e.g., Grammar.opts)
Object opts = null;
try {
for (Field field : Class.forName(className).getDeclaredFields()) {
if (!"opts".equals(field.getName())) continue;
opts = field.get(null);
}
} catch (Throwable t) {
System.out.println("Problem processing: " + line);
throw new RuntimeException(t);
}
if (opts != null) {
args.add(group);
args.add(opts);
}
}
parser.registerAll(args.toArray(new Object[0]));
return parser;
}
}