Friday, March 27, 2009

Application Scripting with the Jess Language

Early on in my running example, the SINFERS soil property expert system that I've been working on for 2+ years now, we encountered the need for a console user interface to drive our prototype during testing and to give proof-of-concept demos.

Arguably, there are many ways to skin this cat (sorry PETA folks!), but what is the quickest and easiest? What is simple enough to meet the immediate need, but also scalable should we decide to keep it? Given our time constraints, we don't have the luxury of expendable code.

Rejected Approaches

The first thought was to write a command line parser in ANTLR, but this was quickly determined not to be worth the time and effort involved since it would require rather special knowledge of ANTLR to maintain and expand.

Second, we looked at using the Jython language, since the Jython API includes the org.python.util.PythonInterpreter class for embedding Jython scripting in a Java application and since we were already using Jython for doing automated rule generation from our pedotransfer function database. This worked well for driving the SINFERS API for a while, but that meant that we'd have to depend on later developers knowing Java, Jython, and Jess. What we really needed was transparent access to SINFERS and Jess via a command line, keeping dependencies on other libraries to a minimum.

Then a crazy idea occurred to me: Just use Jess!

Jess as a General Purpose Scripting Language

Like Dorothy, I had had the power to go back to Kansas all along, I just didn't see it.

The reason is simple: 99% of the time, I use the Jess language for the mundane tasks of scripting rules, defining modules, deftemplates, and all the other constructs and initialization stuff -- all as you're supposed to do. However, the Jess language is much more that just a driver of the Jess API, it can script the Java language itself.

As Ernest is want to say, "Anything that you can do with Java you can do with Jess."

Now, one of the neat things about Jess is that is extremely easy to add new functionality. All you have to do to add a function to Jess is create a new command class that implements the jess.Userfunction interface. For greater convenience, these functions can be loaded via a jess.Userpackage class during application initialization.

This is great if you want to call new, custom functions from Jess script during debugging and general development, but what if Jess is acting as an embedded component in a larger system?

What we decided to do was to create a number of commands classes implementing the jess.Userfunction interface, add them to a userpackage, and make the userpackage an internal implementation detail of the SINFERS API. This gives us a simple and robust means of adding commands to SINFERS. The sinfers.core.commandsCmdImportModel class is shown in Figure 1 as an example.



public class CmdImportModel implements Userfunction {

public Value call(ValueVector vv, Context context) throws JessException {
String successMessage = null;
log.debug("CALLING COMMAND: import-model...");
Object obj = vv.get(1).javaObjectValue(context);
Rete engine = context.getEngine();
Object model = null;
Sinfers sinfers = (Sinfers) (engine.fetch("SINFERS")
.javaObjectValue(context));
if (obj instanceof java.io.File) {
File f = (File) obj;
model = sinfers.importModel(f);
try {
successMessage = "Model imported from " + f.getCanonicalPath() + " OK!";
} catch (IOException e) {
log.error("Model imported failed! Could not find canonical file path.");
e.printStackTrace(System.err);
}
} else if (obj instanceof java.net.URL) {
URL url = (URL) obj;
model = sinfers.importModel(url);
successMessage = "Model imported from " + url.toString() + " OK!";
} else if (obj instanceof java.lang.String) {
String s = (String) obj;
model = sinfers.importModel(s);
successMessage = "Model imported from " + s + " OK!";
} else {
log.debug("Unrecognized argument to import-model command.");
throw new JessException(
"Unrecognized argument to import-model command", null, 0);
}
log.debug("Model imported OK.");
System.out.println(successMessage);
return new Value(model);
}

public String getName() {
return "import-model";
}
}


Figure 1. The SINFERS CmdImportModel class.

Just as Jess has a Main class, so to does SINFERS, which drives an instance of the sinfers.main.Sinfers class. Sinfers is the true application class in the SINFERS API - a facade much like the Rete class in the Jess API.

The Rete method eval() is able to parse and execute functional expressions. So, since each userfunction fits that description, we can pass our SINFERS commands to Jess from the console and have them executed by Rete.eval(). The net effect is that Jess is scripting our API, giving us the ability to run either one SINFERS commands or Jess's commands at the SINFERS command line.

Figure 2 shows the result of having imported one of our soil property data files for analysis. Note the seamless extension with Jess proper.



Figure 2. Using Jess to provide a simple, embedded, command line interface.


Of course, I could have hacked Jess's source to change the prompt and all that, but the problem is more than cosmetic. Among other things, we wanted to reserve the right to pass arguments, expressions, and switches on the command line that Jess eval() cannot accept. [I suppose I could just add more userfunctions for these special cases too, eh? -JM]

The really cool thing is that the Rete instance that is powering the SINFERS command line in this session is the same instance that this session uses for its inferencing -- Jess is pulling double duty! But it gets cooler! Can SINFERS run a script of its commands? You bet! Jess doesn't care. Just like writing any CLP file of Jess code, I can mix and match SINFERS commands with native Jess commands for true shell scripting. See Figure 3 for an example script.


;; test.clp
;; ========
;; Imports and analyzes a SINFERS model file.
(watch all)
(bind ?model (import-model "simpleModel.xml"))
(add-model ?model)
(run-sinfers)
(facts *)


Figure 3. A SINFERS script file.

As far as I know, this is a novel way of exploiting the power of the Jess language to control the API of a host application. - JM

No comments: