package edu.stanford.nlp.sempre.interactive;
import java.io.PrintWriter;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import com.google.common.collect.Lists;
import edu.stanford.nlp.sempre.Builder;
import edu.stanford.nlp.sempre.ContextValue;
import edu.stanford.nlp.sempre.Derivation;
import edu.stanford.nlp.sempre.Example;
import edu.stanford.nlp.sempre.Formula;
import edu.stanford.nlp.sempre.Formulas;
import edu.stanford.nlp.sempre.Master;
import edu.stanford.nlp.sempre.Params;
import edu.stanford.nlp.sempre.Parser;
import edu.stanford.nlp.sempre.ParserState;
import edu.stanford.nlp.sempre.Rule;
import edu.stanford.nlp.sempre.RuleSource;
import edu.stanford.nlp.sempre.Session;
import fig.basic.IOUtils;
import fig.basic.LispTree;
import fig.basic.LogInfo;
import fig.basic.Option;
import fig.basic.Ref;
/**
* An InteractiveMaster supports interactive commands, and grammar induction
* methods.
*/
public class InteractiveMaster extends Master {
public static class Options {
@Option(gloss = "Write out new grammar rules")
public String intOutputPath;
@Option(gloss = "each session gets a different model with its own parameters")
public boolean independentSessions = false;
@Option(gloss = "number of utterances to return for autocomplete")
public int autocompleteCount = 5;
@Option(gloss = "only allow interactive commands")
public boolean onlyInteractive = false;
@Option(gloss = "try partial matches")
public boolean useAligner = true;
@Option(gloss = "use the best formula when no match or not provided")
public int maxSequence = 20;
@Option(gloss = "path to the citations")
public int maxChars = 200;
@Option(gloss = "allow regular commands specified in Master")
public boolean allowRegularCommands = false;
}
public static Options opts = new Options();
public InteractiveMaster(Builder builder) {
super(builder);
}
@Override
protected void printHelp() {
// interactive commands
LogInfo.log("Interactive commands");
LogInfo.log(
" (:def head [[body1,bodyformula1],[body2,bodyformula2]]): provide a definition for the original utterance");
LogInfo.log(" (:q |utterance|): provide a definition for the original utterance");
LogInfo.log(" (:accept |formula1| |formula2|): accept any derivation with those corresponding formula");
LogInfo.log(" (:reject |formula1| |formula2|): reject any derivations with those corresponding formula");
LogInfo.log("Main commands:");
super.printHelp();
}
@Override
public void runServer() {
InteractiveServer server = new InteractiveServer(this);
server.run();
}
@Override
public Response processQuery(Session session, String line) {
LogInfo.begin_track("InteractiveMaster.handleQuery");
LogInfo.logs("session %s", session.id);
LogInfo.logs("query %s", line);
line = line.trim();
Response response = new Response();
if (line.startsWith("(:"))
handleCommand(session, line, response);
else if (line.startsWith("(") && opts.allowRegularCommands || session.id.equals("stdin"))
super.processQuery(session, line);
else
handleCommand(session, String.format("(:q \"%s\")", line), response);
LogInfo.end_track();
return response;
}
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;
QueryStats stats = new QueryStats(response, command);
// Start of interactive commands
if (command.equals(":q")) {
// Create example
String utt = tree.children.get(1).value;
Example ex = exampleFromUtterance(utt, session);
if (!utteranceAllowed(ex, response)) {
stats.error("utterance_too_expensive");
// returns with size and error message
return;
}
builder.parser.parse(builder.params, ex, false);
stats.size(ex.predDerivations != null ? ex.predDerivations.size() : 0);
stats.status(InteractiveUtils.getParseStatus(ex));
LogInfo.logs("parse stats: %s", response.stats);
response.ex = ex;
} else if (command.equals(":qdbg")) {
// Create example
String utt = tree.children.get(1).value;
Example ex = exampleFromUtterance(utt, session);
builder.parser.parse(builder.params, ex, false);
Derivation.opts.showCat = true;
Derivation.opts.showRules = true;
for (Derivation d : ex.predDerivations) {
response.lines.add(d.toLispTree().toString());
}
Derivation.opts.showCat = false;
Derivation.opts.showRules = false;
response.ex = ex;
} else if (command.equals(":reject")) {
stats.put("rejectSize", tree.children.size());
} else if (command.equals(":accept")) {
String utt = tree.children.get(1).value;
List<Formula> targetFormulas = new ArrayList<>();
try {
targetFormulas = tree.children.subList(2, tree.children.size()).stream()
.map(t -> Formulas.fromLispTree(LispTree.proto.parseFromString(t.value))).collect(Collectors.toList());
} catch (Exception e) {
e.printStackTrace();
response.lines.add("cannot accept formula: ");
}
Example ex = exampleFromUtterance(utt, session);
response.ex = ex;
// Parse!
((InteractiveBeamParser)builder.parser).parseWithoutExecuting(builder.params, ex, false);
int rank = -1;
Derivation match = null;
for (int i = 0; i < ex.predDerivations.size(); i++) {
Derivation derivi = ex.predDerivations.get(i);
if (targetFormulas.contains(derivi.formula)) {
rank = i;
match = derivi;
break;
}
}
if (rank == -1) {
stats.error("unable to match on accept");
}
stats.rank(rank);
stats.status(InteractiveUtils.getParseStatus(ex));
stats.size(ex.predDerivations.size());
stats.put("formulas.size", targetFormulas.size());
stats.put("len_formula", targetFormulas.get(0).toLispTree().toString().length());
stats.put("len_utterance", ex.utterance.length());
if (match != null) {
if (session.isWritingCitation()) {
InteractiveUtils.cite(match, ex);
}
// ex.setTargetValue(match.value); // this is just for logging, not
// actually used for learning
if (session.isLearning()) {
LogInfo.begin_track("Updating parameters");
learner.onlineLearnExampleByFormula(ex, targetFormulas);
LogInfo.end_track();
}
}
} else if (command.startsWith(":def")) {
stats.put("type", "def"); // startsWith
if (tree.children.size() == 3) {
String head = tree.children.get(1).value;
String jsonDef = tree.children.get(2).value;
List<Rule> inducedRules = new ArrayList<>();
stats.put("head_len", head.length());
stats.put("json_len", jsonDef.length());
try {
inducedRules.addAll(induceRulesHelper(command, head, jsonDef, builder.parser, builder.params, session,
new Ref<Response>(response)));
stats.put("num_rules", inducedRules.size());
} catch (BadInteractionException e) {
stats.put("num_rules", 0);
stats.error(e.getMessage());
response.lines.add(e.getMessage());
return;
}
if (inducedRules.size() > 0) {
if (session.isLearning()) {
for (Rule rule : inducedRules) {
InteractiveUtils.addRuleInteractive(rule, builder.parser);
}
stats.put("total_rules", ((InteractiveBeamParser)builder.parser).allRules.size());
stats.put("total_unicat", ((InteractiveBeamParser)builder.parser).interactiveCatUnaryRules.size());
}
// TODO : should not have to parse again, I guess just set the formula
// or something
// builder.parser.parse(builder.params, refExHead.value, false);
// write out the grammar
if (session.isWritingGrammar()) {
PrintWriter out = IOUtils
.openOutAppendHard(Paths.get(InteractiveMaster.opts.intOutputPath, "grammar.log.json").toString());
for (Rule rule : inducedRules) {
out.println(rule.toJson());
}
out.close();
}
} else {
LogInfo.logs("No rule induced for head %s", head);
}
} else {
LogInfo.logs("Invalid format for def");
}
} else if (command.equals(":printInfo")) {
LogInfo.logs("Printing and overriding grammar and parameters...");
builder.params.write(Paths.get(InteractiveMaster.opts.intOutputPath, "params.params").toString());
PrintWriter out = IOUtils
.openOutAppendHard(Paths.get(InteractiveMaster.opts.intOutputPath + "grammar.final.json").toString());
for (Rule rule : builder.grammar.getRules()) {
out.println(rule.toJson());
}
out.close();
LogInfo.logs("Done printing and overriding grammar and parameters...");
} else if (command.equals(":context")) {
if (tree.children.size() == 1) {
LogInfo.logs("%s", session.context);
} else {
session.context = ContextValue
.fromString(String.format("(context (graph NaiveKnowledgeGraph ((string \"%s\") (name b) (name c))))",
tree.children.get(1).toString()));
response.stats.put("context_length", tree.children.get(1).toString().length());
}
} else {
LogInfo.log("Invalid command: " + tree);
}
}
private static Example exampleFromUtterance(String utt, Session session) {
Example.Builder b = new Example.Builder();
b.setId(session.id);
b.setUtterance(utt);
b.setContext(session.context);
Example ex = b.createExample();
ex.preprocess();
return ex;
}
public static List<Rule> induceRulesHelper(String command, String head, String jsonDef, Parser parser, Params params,
Session session, Ref<Response> refResponse) throws BadInteractionException {
Example exHead = exampleFromUtterance(head, session);
LogInfo.logs("head: %s", exHead.getTokens());
if (exHead.getTokens() == null || exHead.getTokens().size() == 0)
throw BadInteractionException.headIsEmpty(head);
if (isNonsense(exHead))
throw BadInteractionException.nonSenseDefinition(head);
InteractiveBeamParserState state = ((InteractiveBeamParser)parser).parseWithoutExecuting(params, exHead, false);
if (GrammarInducer.getParseStatus(exHead) == GrammarInducer.ParseStatus.Core)
throw BadInteractionException.headIsCore(head);
LogInfo.logs("num anchored: %d", state.chartList.size());
List<String> bodyList = InteractiveUtils.utterancefromJson(jsonDef, false);
LogInfo.logs("bodyutterances:\n %s", String.join("\t", bodyList));
Derivation bodyDeriv = InteractiveUtils
.combine(InteractiveUtils.derivsfromJson(jsonDef, parser, params, refResponse));
if (refResponse != null) {
refResponse.value.ex = exHead;
}
List<Rule> inducedRules = new ArrayList<>();
GrammarInducer grammarInducer = new GrammarInducer(exHead.getTokens(), bodyDeriv, state.chartList);
inducedRules.addAll(grammarInducer.getRules());
for (Rule rule : inducedRules) {
rule.source = new RuleSource(session.id, head, bodyList);
}
if (opts.useAligner && bodyList.size() == 1) {
List<Rule> alignedRules = DefinitionAligner.getRules(exHead.getTokens(),
InteractiveUtils.utterancefromJson(jsonDef, true), bodyDeriv, state.chartList);
for (Rule rule : alignedRules) {
rule.source = new RuleSource(session.id, head, bodyList);
rule.source.align = true;
}
inducedRules.addAll(alignedRules);
}
exHead.predDerivations = Lists.newArrayList(bodyDeriv);
return inducedRules;
}
private static boolean isNonsense(Example exHead) {
List<String> tokens = exHead.getTokens();
if (tokens.size() > 10)
return true;
if (tokens.size() == 0)
return true;
return tokens.stream().anyMatch(s -> s.length() > 15);
}
private boolean utteranceAllowed(Example ex, Response response) {
if (ex.utterance.length() > opts.maxChars) {
response.lines.add(String.format("refused to execute: too many characters in one command (current: %d, max: %d)",
ex.utterance.length(), opts.maxChars));
return false;
}
long approxSeq = ex.getLemmaTokens().stream().filter(s -> s.contains(";")).count();
if (approxSeq >= opts.maxSequence) {
response.lines.add(String.format(
"refused to execute: too many steps in one command -- "
+ "consider defining some of steps as one single step. (current: %d, max: %d)",
approxSeq, opts.maxSequence));
return false;
}
return true;
}
}