package edu.neu.ccs.task.agent;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import org.mozilla.javascript.RhinoException;
import edu.neu.ccs.task.DecompositionType;
import edu.neu.ccs.task.Plan;
import edu.neu.ccs.task.Priority;
import edu.neu.ccs.task.Task;
import edu.neu.ccs.task.TaskEngine;
import edu.neu.ccs.task.TaskModel;
import edu.neu.ccs.task.TaskModelSet;
import edu.neu.ccs.task.TaskPlan;
import edu.neu.ccs.task.TaskType;
import edu.neu.ccs.task.util.Console;
import edu.neu.ccs.task.util.Decorator;
import edu.neu.ccs.task.util.Desc;
import edu.neu.ccs.task.util.DotUtils;
import edu.neu.ccs.task.util.Dottable;
import edu.neu.ccs.task.util.Help;
import edu.neu.ccs.task.util.Pair;
import edu.neu.ccs.task.util.XMLUtil;
/**
* A console-based interface to the dialogue agent.
*
* @author Dan Schulman
* @version $Id: AgentConsole.java 949 2010-09-12 19:13:24Z schulman $
*/
public class AgentConsole extends Console {
private final Agent agent;
private boolean verbose = false;
private boolean autostatus = false;
/**
* Create a new agent console, writing to the specified log file.
*
* @param logFile path to a log file.
* @throws IOException
*/
public AgentConsole(String logfile) throws IOException {
super(logfile);
this.agent = new Agent();
}
public AgentConsole(String logfile, TaskEngine engine) throws IOException {
super(logfile);
this.agent = new Agent(engine);
}
protected Agent getAgent() {
return agent;
}
protected TaskEngine getEngine() {
return agent.getEngine();
}
protected TaskModelSet getModelSet() {
return agent.getModelSet();
}
@Override
public void run() throws IOException {
printBanner();
super.run();
}
protected void printBanner() {
out.println("DTask dialogue engine");
out.println("Version " + Agent.VERSION);
out.println("-----------------------");
}
@Override
protected void system() {
while (!agent.hasCurrentTurn() && agent.isLive()) {
boolean hasTurn = agent.nextTurn();
showAutostatus();
if (hasTurn)
performTurn();
}
}
private void performTurn() {
String output = verbose ?
agent.getCurrentAnnotatedOutput() :
agent.getCurrentPlainOutput();
if (output != null)
out.println("AGENT: " + output);
if ( ! showUserInputRequest()) {
agent.applyCurrentTurn();
showAutostatus();
}
}
private void performRepeat() {
String output = verbose ?
agent.getCurrentAnnotatedAltOutput() :
agent.getCurrentPlainAltOutput();
out.println("AGENT: " + output);
showUserInputRequest();
}
private boolean showUserInputRequest() {
List<Pair<String,Integer>> inputs = agent.getCurrentInputs();
if (inputs != null) {
showUserChoices(inputs);
return true;
}
String prompt = agent.getCurrentInputPrompt();
if (prompt != null) {
showUserPrompt(prompt);
return true;
}
Pair<String, Map<String,String>> widget= agent.getCurrentInputWidget();
if (widget != null) {
showUserWidget(widget);
return true;
}
return false;
}
private void showUserChoices(List<Pair<String, Integer>> inputs) {
int i=1;
for (Pair<String, Integer> p : inputs)
out.printf("%d. %s\n", i++, p.first);
}
private void showUserPrompt(String prompt) {
out.printf("TEXTINPUT: %s\n", prompt);
}
private void showUserWidget(Pair<String, Map<String,String>> w) {
out.printf("WIDGET: %s\n", w.first);
for (Map.Entry<String, String> e : w.second.entrySet())
out.printf("%s=%s\n", e.getKey(), e.getValue());
}
@Override
protected void reportException(Exception e) {
// Try to do a good job of reporting javascript exceptions.
// We get better messages if we look for the Rhino-specific exceptions
for (Throwable t = e; t != null; t = t.getCause())
if (t instanceof RhinoException) {
RhinoException re = (RhinoException) t;
out.println("Javascript error: " + re.getMessage());
return;
}
super.reportException(e);
}
@Desc("evaluate a Javascript expression")
@Help("Usage: eval EXPR\n" +
"EXPR is evaluated as a Javascript expression, and the\n" +
"results are printed. Evaluation is in the global \n" +
"environment, so $this is not bound.")
public void _eval(String expr) {
TaskEngine e = getEngine();
Object value = e.evalGlobal(expr, "console eval");
if (value != null)
out.println(e.asLiteral(value, "console eval"));
}
@Desc("load a model file")
@Help("Usage: load FILE\n" +
"Usage: load PREFIX=FILE\n" +
"A task model is loaded from FILE, which may be either a path\n" +
"or a URL. If PREFIX is supplied, then this prefix is bound\n" +
"to the model's URI, and can be used to refer to any tasks or\n" +
"task decompositions within in. If no prefix is supplied, a\n" +
"default one will be created.\n" +
"Loaded a model file causes any initialization scripts in the\n" +
"model to be executed.\n" +
"If a model file with the same URI has already been loaded,\n" +
"the existing model file will first be unloaded.")
public void _load(String model) throws IOException, XMLStreamException {
if ( ! XMLUtil.validate(model, XMLUtil.getDialogueSchema()))
return;
String prefix = null;
String[] parts = model.split("=", 2);
if (parts.length == 2) {
prefix = parts[0].trim();
model = parts[1].trim();
}
TaskModel m = getEngine().loadAndInit(model, prefix);
if (m.getLoadCount() > 1)
out.printf("Reloaded %s=%s\n", m.getPrefix(), m.getURI());
else
out.printf("Loaded %s=%s\n", m.getPrefix(), m.getURI());
}
@Desc("remove a task model from the runtime")
@Help("Usage: unload PREFIX\n" +
"The model bound to PREFIX is removed from the runtime.\n" +
"All tasks, decompositions, scripts, and dialogue contained in\n" +
"the model will no longer be accessible.\n" +
"Note that any existing task instances in the current plan tree\n" +
"will still be present -- however new instances cannot be\n" +
"created")
public void _unload(String prefix) {
if ("".equals(prefix))
prefix = getModelSet().getDefaultPrefix();
TaskModel model = getModelSet().getModelForPrefix(prefix);
if (model == null)
throw new IllegalArgumentException("unknown model: " + prefix);
getModelSet().unload(model.getURI());
out.printf("Unloaded %s=%s\n", prefix, model.getURI());
}
@Desc("reload a task model from its original location")
@Help("Usage: reload\n" +
"Usage: reload PREFIX\n" +
"Reload the model bound to PREFIX, or the current default\n" +
"model, if no PREFIX is supplied.\n" +
"This is equivalent to unloading a model, following by\n" +
"loading it again (from the same location as before).\n" +
"Note that any task instances in the current plan tree\n" +
"will continue to refer to the previous copy of the model.")
public void _reload(String prefix) throws IOException, XMLStreamException {
if ("".equals(prefix))
prefix = getModelSet().getDefaultPrefix();
TaskModel model = getModelSet().getModelForPrefix(prefix);
if (model == null)
throw new IllegalArgumentException("unknown model: " + prefix);
_load(model.getLocation());
}
@Desc("set the default model")
@Help("Usage: model PREFIX\n" +
"The model bound to PREFIX becomes the default model.\n" +
"This means that the tasks contained in this model may\n" +
"be used in commands such as 'run' without specifying a\n" +
"prefix.")
public void _model(String prefix) {
TaskModel model = getModelSet().getModelForPrefix(prefix);
if (model == null)
throw new IllegalArgumentException("unknown model: " + prefix);
getModelSet().setDefaultPrefix(prefix);
out.printf("Using %s=%s\n", prefix, model.getURI());
}
@Desc("display loaded models")
@Help("Usage: models\n" +
"Lists all loaded models, and the prefix for each.")
public void _models(String ignored) {
if (getModelSet().getModels().isEmpty())
out.println("no models loaded");
for (TaskModel m : getModelSet().getModels()) {
if (m.getPrefix().equals(getModelSet().getDefaultPrefix()))
out.print('*'); // highlight the current default namespace
out.printf("%s=%s\n", m.getPrefix(), m.getURI());
}
}
@Desc("start a new top-level task")
@Help("Usage: run TASK (; INPUT=VALUE)*\n" +
"Create a new instance of TASK, and start it as a top-level\n" +
"task, replacing the current plan tree with one rooted at the\n" +
"new instance.\n" +
"TASK will be found in the current default model, unless a \n" +
"prefix is supplied (see 'model').\n" +
"For each INPUT=VALUE provided, VALUE will be evaluated as a\n" +
"Javascript expression, and this value will be bound to the\n" +
"input slot identified by INPUT. It is an error to leave any\n" +
"input slots unbound.")
public void _run(String str) {
Map<String, String> slots = new HashMap<String, String>();
String name = parseTask(str, slots);
TaskPlan plan = getEngine().newTaskPlan(
XMLUtil.parseQName(getModelSet(), name));
Task task = plan.getTask();
for (Map.Entry<String, String> e : slots.entrySet())
task.setSlotValueScript(e.getKey(), e.getValue(), "run task");
if (plan.isStartable())
agent.setFocus(plan);
else {
List<String> undef = task.getUndefinedInputs();
if (undef.isEmpty())
out.println("Cannot run " + task + ": precondition not met");
else
out.println("Undefined input: " + undef);
}
}
private static String parseTask(String s, Map<String, String> slots) {
String[] args = s.split(";");
for (int n=1; n<args.length; n++) {
String[] parts = args[n].split("=", 2);
if (parts.length != 2)
throw new IllegalArgumentException("Bad argument: " + args[n]);
slots.put(parts[0].trim(), parts[1]);
}
return args[0].trim();
}
@Desc("choose user dialogue, or display possible choices")
@Help("Usage: say\n" +
"Display possible choices of user dialogue.\n" +
"Usage: say NUMBER\n" +
"Choose a user utterance, where NUMBER is one of the choices\n" +
"displayed by 'say' with no argument.")
public void _say(String choiceStr) {
// First try it as a numeric choice:
int choice;
try {
choice = Integer.parseInt(choiceStr) - 1;
} catch (NumberFormatException e) {
choice = -1;
}
List<Pair<String, Integer>> inputs = agent.getCurrentInputs();
if ((inputs != null) && (choice >= 0) && (choice<inputs.size())) {
int value = inputs.get(choice).second;
if (value < 0)
performRepeat();
else {
agent.applyCurrentTurn(value);
showAutostatus();
}
return;
}
if (agent.getCurrentInputPrompt()!=null || agent.getCurrentInputWidget()!=null) {
agent.applyCurrentTurn(choiceStr);
showAutostatus();
return;
}
if ( ! showUserInputRequest())
out.println("not expecting user input");
}
@Desc("display the current plan tree")
@Help("Usage: status\n" +
"Display a text representation of the current plan tree.\n" +
"If 'verbose' is enabled, this will contain more detail.")
public void _status(String ignored) {
Plan top = agent.getTop();
if (top == null)
out.println("No current task");
else
top.printTree(out, verbose, new Decorator() {
public String decorate(Object o) {
return (o == agent.getFocus()) ? " <-focus" : "";
}
});
}
@Desc("display the current plan tree at each change")
@Help("Usage: autostatus (true | false)\n" +
"When autostatus mode is on, display the current plan tree\n" +
"(as with status) each time it changes.")
public void _autostatus(String arg) {
arg = arg.trim();
autostatus = "".equals(arg) || Boolean.parseBoolean(arg);
out.println("Autostatus " + (autostatus ? "on" : "off"));
}
private void showAutostatus() {
if (autostatus) {
out.println();
_status(null);
}
}
@Desc("display additional detail")
@Help("Usage: verbose (true | false)\n" +
"When verbose mode is on, some commands (notable 'status')\n" +
"display additional detail.")
public void _verbose(String arg) {
arg = arg.trim();
verbose = "".equals(arg) || Boolean.parseBoolean(arg);
out.println("Verbose " + (verbose ? "on" : "off"));
}
@Desc("create a decomposition graph")
@Help("Usage: dotdecomp TASK FILE\n" +
"Usage: dotdecomp DECOMPOSITION FILE\n" +
"Create a directed graph representation of all tasks and\n" +
"decompositions reachable starting from TASK or DECOMPOSITION.\n" +
"In this graph, each task and decomposition is a node, and\n" +
"there is an edge from each task to its decomposition, and each\n" +
"decomposition to its steps.\n" +
"The graph is written in Graphviz 'dot' format. See\n" +
"http://www.graphviz.org for visualization tools which process\n" +
"this format.")
public void _dotdecomp(String arg) throws IOException, InterruptedException {
String[] args = arg.trim().split("\\s+", 2);
QName name = XMLUtil.parseQName(getModelSet(), args[0]);
Dottable dot = null;
TaskType task = getModelSet().getTaskType(name);
if (task != null)
dot = task.decompositionGraph();
else {
DecompositionType decomp = getModelSet().getDecompositionType(name);
if (decomp != null)
dot = decomp.decompositionGraph();
}
if (dot == null)
out.println("Unknown ID: " + arg);
else if (args.length < 2)
DotUtils.showGraph(dot);
else {
PrintWriter w = new PrintWriter(args[1]);
try {
dot.printDot(w);
} finally {
w.close();
}
}
}
@Desc("create a bindings graph for a decomposition")
@Help("Usage: dotbindings DECOMPOSITION FILE\n" +
"Create a graph representation of the data flow from inputs\n" +
"to outputs within a particular task decomposition.\n" +
"The graph is written in Graphviz 'dot' format. See\n" +
"http://www.graphviz.org for visualization tools which process\n" +
"this format.")
public void _dotbindings(String arg) throws IOException, InterruptedException {
String[] args = arg.trim().split("\\s+", 2);
QName name = XMLUtil.parseQName(getModelSet(), args[0]);
DecompositionType type = getModelSet().getDecompositionType(name);
if (type == null)
out.println("Unknown ID: " + arg);
else if (args.length < 2)
DotUtils.showGraph(type.bindingGraph());
else {
PrintWriter w = new PrintWriter(args[1]);
try {
type.bindingGraph().printDot(w);
} finally {
w.close();
}
}
}
@Desc("perform static analysis of task models")
@Help("Usage: check\n" +
"Check all currently loaded task models for some easily-\n" +
"diagnosed errors, including:\n" +
"- Task decompositions must refer to a known task.\n" +
"- Tasks must have known decompositions, scripts, or dialogue.\n" +
"- Decomposition steps must refer to known tasks.\n" +
"- Decomposition bindings must refer to known inputs/outputs.\n" +
"- Decompositions must bind all outputs of a task.")
public void _check(String arg) {
List<String> errors = new ArrayList<String>();
getModelSet().checkAll(errors);
for (String err : errors)
out.println(err);
}
@Desc("set the random number generator seed")
@Help("Usage: randseed SEED\n" +
"Set the seed used for all psuedorandomness, including\n" +
"choosing from multiple recipes and/or dialogue turns.\n" +
"This is useful for creating reproducible test cases.")
public void _randseed(String arg) {
Priority.setRandSeed(Long.parseLong(arg));
}
public static void main(String[] args) throws Exception {
AgentMain.consoleMain(args);
}
}