package org.myrobotlab.service;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.alicebot.ab.AIMLMap;
import org.alicebot.ab.AIMLSet;
import org.alicebot.ab.Bot;
import org.alicebot.ab.Category;
import org.alicebot.ab.Chat;
import org.alicebot.ab.Predicates;
import org.apache.commons.io.IOUtils;
import org.myrobotlab.framework.Service;
import org.myrobotlab.framework.ServiceType;
import org.myrobotlab.logging.LoggerFactory;
import org.myrobotlab.logging.LoggingFactory;
import org.myrobotlab.programab.ChatData;
import org.myrobotlab.programab.OOBPayload;
import org.myrobotlab.service.interfaces.ServiceInterface;
import org.myrobotlab.service.interfaces.TextListener;
import org.myrobotlab.service.interfaces.TextPublisher;
import org.slf4j.Logger;
/**
* Program AB service for MyRobotLab Uses AIML 2.0 to create a ChatBot This is a
* reboot of the Old AIML spec to be more 21st century.
*
* More Info at http://aitools.org/ProgramAB
*
* @author kwatters
*
*/
public class ProgramAB extends Service implements TextListener, TextPublisher {
transient public final static Logger log = LoggerFactory.getLogger(ProgramAB.class);
public static class Response {
public String session;
public String msg;
public List<OOBPayload> payloads;
// FIXME - timestamps are usually longs System.currentTimeMillis()
public Date timestamp;
public Response(String session, String msg, List<OOBPayload> payloads, Date timestamp) {
this.session = session;
this.msg = msg;
this.payloads = payloads;
this.timestamp = timestamp;
}
public String toString() {
return String.format("%d %s %s", timestamp.getTime(), session, msg);
}
}
transient Bot bot = null;
HashSet<String> bots = new HashSet<String>();
String path = "ProgramAB";
/**
* botName - is un-initialized to preserve serialization stickyness
*/
// String botName;
// This is the username that is chatting with the bot.
// String currentSession = "default";
// Session is a user and a bot. so the key to the session should be the
// username, and the bot name.
HashMap<String, ChatData> sessions = new HashMap<String, ChatData>();
// TODO: better parsing than a regex...
transient Pattern oobPattern = Pattern.compile("<oob>.*?</oob>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
transient Pattern mrlPattern = Pattern.compile("<mrl>.*?</mrl>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
// a guaranteed bot we have
String currentBotName = "alice2";
// this is the username that is chatting with the bot.
String currentUserName = "default";
static final long serialVersionUID = 1L;
static int savePredicatesInterval = 60 * 1000 * 5; // every 5 minutes
public ProgramAB(String name) {
super(name);
// Tell programAB to persist it's learned predicates about people
// every 30 seconds.
addTask("savePredicates", savePredicatesInterval, "savePredicates");
// TODO: Lazy load this!
// look for local bots defined
File programAbDir = new File(String.format("%s/bots", path));
if (!programAbDir.exists() || !programAbDir.isDirectory()) {
log.info("%s does not exist !!!");
} else {
File[] listOfFiles = programAbDir.listFiles();
for (int i = 0; i < listOfFiles.length; i++) {
if (listOfFiles[i].isFile()) {
// System.out.println("File " + listOfFiles[i].getName());
} else if (listOfFiles[i].isDirectory()) {
bots.add(listOfFiles[i].getName());
}
}
}
}
public void addOOBTextListener(TextListener service) {
addListener("publishOOBText", service.getName(), "onOOBText");
}
public void addResponseListener(Service service) {
addListener("publishResponse", service.getName(), "onResponse");
}
public void addTextListener(TextListener service) {
addListener("publishText", service.getName(), "onText");
}
public void addTextPublisher(TextPublisher service) {
addListener("publishText", service.getName(), "onText");
}
private void cleanOutOfDateAimlIFFiles(String botName) {
String aimlPath = path + File.separator + "bots" + File.separator + botName + File.separator + "aiml";
String aimlIFPath = path + File.separator + "bots" + File.separator + botName + File.separator + "aimlif";
log.info("AIML FILES:");
File folder = new File(aimlPath);
if (!folder.exists()) {
log.info("{} does not exist", aimlPath);
return;
}
log.info(folder.getAbsolutePath());
HashMap<String, Long> modifiedDates = new HashMap<String, Long>();
for (File f : folder.listFiles()) {
log.info(f.getAbsolutePath());
// TODO: better stripping of the file extension
String aiml = f.getName().replace(".aiml", "");
modifiedDates.put(aiml, f.lastModified());
}
log.info("AIMLIF FILES:");
folder = new File(aimlIFPath);
if (!folder.exists()) {
// TODO: throw an exception warn / log ?
log.info("aimlif directory missing,creating it. " + folder.getAbsolutePath());
folder.mkdirs();
return;
}
for (File f : folder.listFiles()) {
log.info(f.getAbsolutePath());
// TODO: better stripping of the file extension
String aimlIF = f.getName().replace(".aiml.csv", "");
Long lastMod = modifiedDates.get(aimlIF);
if (lastMod != null) {
if (f.lastModified() < lastMod) {
// the AIMLIF file is newer than the AIML file.
// delete the AIMLIF file so ProgramAB recompiles it
// properly.
log.info("Deleteing AIMLIF file because the original AIML file was modified. {}", aimlIF);
f.delete();
}
}
}
}
private String createSessionPredicateFilename(String username, String botName) {
// TODO: sanitize the session label so it can be safely used as a filename
String predicatePath = path + File.separator + "bots" + File.separator + botName + File.separator + "config";
// just in case the directory doesn't exist.. make it.
File predDir = new File(predicatePath);
if (!predDir.exists()) {
predDir.mkdirs();
}
predicatePath += File.separator + username + ".predicates.txt";
return predicatePath;
}
public int getMaxConversationDelay() {
return sessions.get(resolveSessionKey(currentUserName, currentBotName)).maxConversationDelay;
}
public Response getResponse(String text) {
return getResponse(currentUserName, text);
}
/**
*
* @param text
* - the query string to the bot brain
* @param userId
* - the user that is sending the query
* @param robotName
* - the name of the bot you which to get the response from
* @return
*/
public Response getResponse(String username, String botName, String text) {
this.currentBotName = botName;
return getResponse(username, text);
}
public Response getResponse(String username, String text) {
log.info(String.format("Get Response for : user %s bot %s : %s", username, currentBotName, text));
if (bot == null) {
String error = "ERROR: Core not loaded, please load core before chatting.";
error(error);
return new Response(username, error, null, new Date());
}
String sessionKey = resolveSessionKey(username, currentBotName);
if (!sessions.containsKey(sessionKey)) {
startSession(path, username, currentBotName);
}
ChatData chatData = sessions.get(sessionKey);
String res = getChat(username, currentBotName).multisentenceRespond(text);
// grab and update the time when this response came in.
chatData.lastResponseTime = new Date();
// Check the AIML response to see if there is OOB (out of band data)
// If so, publish that data independent of the text response.
List<OOBPayload> payloads = null;
if (chatData.processOOB) {
payloads = processOOB(res);
}
// OOB text should not be published as part of the response text.
Matcher matcher = oobPattern.matcher(res);
res = matcher.replaceAll("").trim();
Response response = new Response(username, res, payloads, chatData.lastResponseTime);
// Now that we've said something, lets create a timer task to wait for N
// seconds
// and if nothing has been said.. try say something else.
// TODO: trigger a task to respond with something again
// if the humans get bored
if (chatData.enableAutoConversation) {
// schedule one future reply. (always get the last word in..)
// int numExecutions = 1;
// TODO: we need a way for the task to just execute one time
// it'd be good to have access to the timer here, but it's transient
addTask("getResponse", chatData.maxConversationDelay, "getResponse", username, text);
}
// EEK! clean up the API!
invoke("publishResponse", response);
invoke("publishResponseText", response);
invoke("publishText", response.msg);
info("to: %s - %s", username, res);
// if (log.isDebugEnabled()) {
// for (String key : sessions.get(session).predicates.keySet()) {
// log.debug(session + " " + key + " " +
// sessions.get(session).predicates.get(key));
// }
// }
// TODO: wire this in so the gui updates properly. ??
// broadcastState();
return response;
}
public String resolveSessionKey(String username, String botname) {
return username + "-" + botname;
}
public Chat getChat(String userName, String botName) {
String sessionKey = resolveSessionKey(userName, botName);
if (!sessions.containsKey(sessionKey)) {
error("%s session does not exist", sessionKey);
return null;
} else {
return sessions.get(sessionKey).chat;
}
}
public void removePredicate(String userName, String predicateName) {
removePredicate(userName, currentBotName, predicateName);
}
public void removePredicate(String userName, String botName, String predicateName) {
Predicates preds = getChat(userName, botName).predicates;
preds.remove(predicateName);
}
public void addToSet(String setName, String setValue) {
// add to the set for the bot.
AIMLSet updateSet = bot.setMap.get(setName);
setValue = setValue.toUpperCase().trim();
if (updateSet != null) {
updateSet.add(setValue);
// persist to disk.
updateSet.writeAIMLSet();
} else {
log.info("Unknown AIML set: {}. A new set will be created. ", setName);
// TODO: should we create a new set ? or just log this warning?
// The AIML Set doesn't exist. Lets create a new one
AIMLSet newSet = new AIMLSet(setName, bot);
newSet.add(setValue);
newSet.writeAIMLSet();
}
}
public void addToMap(String mapName, String mapKey, String mapValue) {
// add an entry to the map.
AIMLMap updateMap = bot.mapMap.get(mapName);
mapKey = mapKey.toUpperCase().trim();
if (updateMap != null) {
updateMap.put(mapKey, mapValue);
// persist to disk!
updateMap.writeAIMLMap();
} else {
log.info("Unknown AIML map: {}. A new MAP will be created. ", mapName);
// dynamically create new maps?!
AIMLMap newMap = new AIMLMap(mapName, bot);
newMap.put(mapKey, mapValue);
newMap.writeAIMLMap();
}
}
public void setPredicate(String username, String predicateName, String predicateValue) {
Predicates preds = getChat(username, currentBotName).predicates;
preds.put(predicateName, predicateValue);
}
public void unsetPredicate(String username, String predicateName) {
Predicates preds = getChat(username, currentBotName).predicates;
preds.remove(predicateName);
}
public String getPredicate(String username, String predicateName) {
Predicates preds = getChat(username, currentBotName).predicates;
return preds.get(predicateName);
}
/**
* Only respond if the last response was longer than delay ms ago
*
* @param session
* - current session/username
* @param text
* - text to get a response for
* @param delay
* - min amount of time that must have transpired since the last
* response.
* @return
*/
public Response getResponse(String session, String text, Long delay) {
ChatData chatData = sessions.get(session);
long delta = System.currentTimeMillis() - chatData.lastResponseTime.getTime();
if (delta > delay) {
return getResponse(session, text);
} else {
return null;
}
}
public boolean isEnableAutoConversation() {
return sessions.get(resolveSessionKey(currentUserName, currentBotName)).enableAutoConversation;
}
public boolean isProcessOOB() {
return sessions.get(resolveSessionKey(currentUserName, currentBotName)).processOOB;
}
/**
* Return a list of all patterns that the AIML Bot knows to match against.
*
* @param botName
* @return
*/
public ArrayList<String> listPatterns(String botName) {
ArrayList<String> patterns = new ArrayList<String>();
for (Category c : bot.brain.getCategories()) {
patterns.add(c.getPattern());
}
return patterns;
}
/**
* Return the number of milliseconds since the last response was given -1 if a
* response has never been given.
*
* @return
*/
public long millisecondsSinceLastResponse() {
ChatData chatData = sessions.get(resolveSessionKey(currentUserName, currentBotName));
if (chatData.lastResponseTime == null) {
return -1;
}
long delta = System.currentTimeMillis() - chatData.lastResponseTime.getTime();
return delta;
}
@Override
public void onText(String text) {
// What else should we do here? seems reasonable to just do this.
// this should actually call getResponse
// on input, get the proper response
// Response resp = getResponse(text);
getResponse(text);
// push that to the next end point.
// invoke("publishText", resp.msg);
}
private OOBPayload parseOOB(String oobPayload) {
// TODO: fix the damn double encoding issue.
// we have user entered text in the service/method
// and params values.
// grab the service
Pattern servicePattern = Pattern.compile("<service>(.*?)</service>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
Matcher serviceMatcher = servicePattern.matcher(oobPayload);
serviceMatcher.find();
String serviceName = serviceMatcher.group(1);
Pattern methodPattern = Pattern.compile("<method>(.*?)</method>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
Matcher methodMatcher = methodPattern.matcher(oobPayload);
methodMatcher.find();
String methodName = methodMatcher.group(1);
Pattern paramPattern = Pattern.compile("<param>(.*?)</param>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL | Pattern.MULTILINE);
Matcher paramMatcher = paramPattern.matcher(oobPayload);
ArrayList<String> params = new ArrayList<String>();
while (paramMatcher.find()) {
// We found some OOB text.
// assume only one OOB in the text?
String param = paramMatcher.group(1);
params.add(param);
}
OOBPayload payload = new OOBPayload(serviceName, methodName, params);
// log.info(payload.toString());
return payload;
// JAXB stuff blows up because the response from program ab is already
// xml decoded!
//
// JAXBContext jaxbContext;
// try {
// jaxbContext = JAXBContext.newInstance(OOBPayload.class);
// Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
// log.info("OOB PAYLOAD :" + oobPayload);
// Reader r = new StringReader(oobPayload);
// OOBPayload oobMsg = (OOBPayload) jaxbUnmarshaller.unmarshal(r);
// return oobMsg;
// } catch (JAXBException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// log.info("OOB tag found, but it's not an MRL tag. {}", oobPayload);
// return null;
}
private List<OOBPayload> processOOB(String text) {
// Find any oob tags
ArrayList<OOBPayload> payloads = new ArrayList<OOBPayload>();
Matcher oobMatcher = oobPattern.matcher(text);
while (oobMatcher.find()) {
// We found some OOB text.
// assume only one OOB in the text?
String oobPayload = oobMatcher.group(0);
Matcher mrlMatcher = mrlPattern.matcher(oobPayload);
while (mrlMatcher.find()) {
String mrlPayload = mrlMatcher.group(0);
OOBPayload payload = parseOOB(mrlPayload);
payloads.add(payload);
// TODO: maybe we dont' want this?
// Notifiy endpoints
invoke("publishOOBText", mrlPayload);
// grab service and invoke method.
ServiceInterface s = Runtime.getService(payload.getServiceName());
if (s == null) {
log.warn("Service name in OOB/MRL tag unknown. {}", mrlPayload);
return null;
}
// TODO: should you be able to be synchronous for this
// execution?
Object result = null;
if (payload.getParams() != null) {
result = s.invoke(payload.getMethodName(), payload.getParams().toArray());
} else {
result = s.invoke(payload.getMethodName());
}
log.info("OOB PROCESSING RESULT: {}", result);
}
}
if (payloads.size() > 0) {
return payloads;
} else {
return null;
}
}
/**
* If a response comes back that has an OOB Message, publish that separately
*
* @param response
* @return
*/
public String publishOOBText(String oobText) {
return oobText;
}
/**
* publishing method of the pub sub pair - with addResponseListener allowing
* subscriptions pub/sub routines have the following pattern
*
* publishing routine -> publishX - must be invoked to provide data to
* subscribers subscription routine -> addXListener - simply adds a Service
* listener to the notify framework any service which subscribes must
* implement -> onX(data) - this is where the data will be sent (the
* call-back)
*
* @param response
* @return
*/
public Response publishResponse(Response response) {
return response;
}
/**
* Test only publishing point - for simple consumers
*
* @param response
* @return
*/
public String publishResponseText(Response response) {
return response.msg;
}
@Override
public String publishText(String text) {
return text;
}
public void reloadSession(String session, String botName) {
reloadSession(path, session, botName);
}
public void reloadSession(String path, String username, String botname) {
// kill the bot
bot = null;
// kill the session
String sessionKey = resolveSessionKey(username, botname);
if (sessions.containsKey(sessionKey)) {
// TODO: will garbage collection clean up the bot now ?
// Or are there other handles to it?
sessions.remove(sessionKey);
}
// TODO: we should make sure we keep the same path as before.
startSession(path, username, currentBotName);
}
/**
* Persist the predicates for all known sessions in the robot.
*
* @throws IOException
*
*/
public void savePredicates() throws IOException {
for (String session : sessions.keySet()) {
// TODO: better parsing of this.
String[] parts = session.split("-");
String username = parts[0];
String botname = parts[1];
String sessionPredicateFilename = createSessionPredicateFilename(username, botname);
File sessionPredFile = new File(sessionPredicateFilename);
Chat chat = getChat(username, botname);
// overwrite the original file , this should always be a full set.
log.info("Writing predicate file for session {}", session);
FileWriter predWriter = new FileWriter(sessionPredFile, false);
for (String predicate : chat.predicates.keySet()) {
String value = chat.predicates.get(predicate);
predWriter.write(predicate + ":" + value + "\n");
}
predWriter.close();
}
log.info("Done saving predicates.");
}
public void setEnableAutoConversation(boolean enableAutoConversation) {
sessions.get(resolveSessionKey(currentUserName, currentBotName)).enableAutoConversation = enableAutoConversation;
}
public void setMaxConversationDelay(int maxConversationDelay) {
sessions.get(resolveSessionKey(currentUserName, currentBotName)).maxConversationDelay = maxConversationDelay;
}
public void setProcessOOB(boolean processOOB) {
sessions.get(resolveSessionKey(currentUserName, currentBotName)).processOOB = processOOB;
}
public void startSession() {
startSession(null);
}
public void startSession(String session) {
startSession(session, currentBotName);
}
public Set<String> getSessionNames() {
return sessions.keySet();
}
/**
* Load the AIML 2.0 Bot config and start a chat session. This must be called
* after the service is created.
*
* @param session
* - The new session name
* @param botName
* - The name of the bot to load. (example: alice2)
*/
public void startSession(String session, String botName) {
startSession(path, session, botName);
}
public void startSession(String path, String userName, String botName) {
this.path = path;
info("starting Chat Session path:%s username:%s botname:%s", path, userName, botName);
this.currentBotName = botName;
this.currentUserName = userName;
// Session is between a user and a bot. key is compound.
String sessionKey = resolveSessionKey(userName, botName);
if (sessions.containsKey(sessionKey)) {
warn("session %s already created", sessionKey);
return;
}
cleanOutOfDateAimlIFFiles(botName);
// TODO: manage the bots in a collective pool/hash map.
if (bot == null) {
bot = new Bot(botName, path);
} else if (!botName.equalsIgnoreCase(bot.name)) {
bot = new Bot(botName, path);
}
Chat chat = new Chat(bot);
// for (Category c : bot.brain.getCategories()) {
// log.info(c.getPattern());
// }
//
// String resp = chat.multisentenceRespond("hello");
// load session specific predicates, these override the default ones.
String sessionPredicateFilename = createSessionPredicateFilename(userName, botName);
chat.predicates.getPredicateDefaults(sessionPredicateFilename);
//
sessions.put(resolveSessionKey(currentUserName, currentBotName), new ChatData(chat));
// lets test if the robot knows the name of the person in the session
String name = chat.predicates.get("name").trim();
// TODO: this implies that the default value for "name" is default
// "Friend"
if (name == null || "Friend".equalsIgnoreCase(name) || "unknown".equalsIgnoreCase(name)) {
// TODO: find another interface that's simpler to use for this
// create a string that represents the predicates file
String inputPredicateStream = "name:" + userName;
// load those predicates
chat.predicates.getPredicateDefaultsFromInputStream(IOUtils.toInputStream(inputPredicateStream));
}
// this.currentBotName = botName;
// String userName = chat.predicates.get("name");
log.info("Started session for bot name:{} , username:{}", botName, userName);
// TODO: to make sure if the start session is updated, that the button
// updates in the gui ?
broadcastState();
}
public void setPath(String path) {
this.path = path;
}
public void writeAIML() {
bot.writeAIMLFiles();
}
public void writeAIMLIF() {
bot.writeAIMLIFFiles();
}
public void writeAndQuit() {
bot.writeQuit();
}
/**
* This static method returns all the details of the class without it having
* to be constructed. It has description, categories, dependencies, and peer
* definitions.
*
* @return ServiceType - returns all the data
*
*/
static public ServiceType getMetaData() {
ServiceType meta = new ServiceType(ProgramAB.class.getCanonicalName());
meta.addDescription("AIML 2.0 Reference interpreter based on Program AB");
meta.addCategory("intelligence");
// meta.addDependency("org.alicebot.ab", "0.0.6.26");
meta.addDependency("org.alicebot.ab", "0.0.1-kw");
meta.addDependency("org.json", "20090211");
return meta;
}
public static void main(String s[]) throws IOException {
LoggingFactory.init("INFO");
// Runtime.createAndStart("gui", "GUIService");
Runtime.createAndStart("webgui", "WebGui");
ProgramAB ai = (ProgramAB) Runtime.createAndStart("simple", "ProgramAB");
// ai.startSession("C:\\mrl\\develop\\ProgramAB","default", "simple");
log.info(ai.getResponse("hi there").toString());
// ai.savePredicates();
}
}