!752 static boolean actuallyPost = true; static L botIDs = litlist("#1001890", "#1001923", "#1001937", "#1001942", "#1001945", "#1001951" /*, "#1001747"*/, "#1001989", "#1001861", "#1001991", "#1001994", "#1002007"); static new L bots; static new L activeBots; static Map botsByID = synchroTreeMap(); static S channelID = "C0FH9PY8J"; // relp, #talkingbots static S token; //static new Map lastPostByUser; static new Map userNames; static S lastQuestion, lastAnswer; static S lastAnswerTimestamp; // only for posted in this session static int lastAnswerScore; static int webServerPort = 80; // set to 0 for no web serving static long hitCount; /* static class Interaction { S question, user, answertime, answer; long realtime; int score; // not used yet } static new L interactions; */ static new L history; // stores all processed user questions (not whole chat log) p { if (token == null) token = loadSecretTextFileMandatory("#1001889", "relp-slack-botstuff-token").trim(); reload(); readLog(); if (webServerPort != 0) { readLocally("hitCount"); serveHttp(webServerPort); } while true { pcall { if (lastAnswerTimestamp != null) { // Poll reactions to our last answer - is there a more efficient way? int score = slackReactionScore(token, channelID, lastAnswerTimestamp); if (score != lastAnswerScore) { print("Reaction score changes to " + score); recordFeedback(lastQuestion, lastAnswer, lastAnswerTimestamp, "emoji", score-lastAnswerScore); lastAnswerScore = score; } } L qs = getNewQuestions(); //print(l(qs) + " question(s)."); //printList(qs); for (SlackMsg msg : qs) { S q = msg.text; S userName = getUserName(msg.user); print("Processing question: " + quote(q)); if (lastAnswer != null) { // TODO: maybe check the time delay int score = feedbackScore(q); if (score != 0) { recordFeedback(lastQuestion /*lastPostByUser.get(userName)*/, lastAnswer, lastAnswerTimestamp, q, score); if (score > 0) pcall { slackSmile(token, channelID, msg.ts); } else pcall { slackReact(token, channelID, msg.ts, "thinking_face"); } } lastAnswer = null; } //if (userName != null) lastPostByUser.put(userName, msg.text); pcall { S a = answer(q); if (a != null) { if (userName != null) a = "<@" + userName + "> " + a; print("Answering with: " + quote(a)); lastAnswer = a; lastQuestion = q; if (actuallyPost) { S answerTime = postAnswer(a); Map logEntry = litmap("question", q, "user", userName, "answertime", answerTime, "answer", a, "realtime", now()); history.add(logEntry); logQuoted(new File(programDir(), "memory-log"), structure(logEntry)); } } } } } sleepSeconds(5); } } // returns the slack timestamp static S postAnswer(S text) { S username = "botstuff"; S url = "https://slack.com/api/chat.postMessage"; S postData = "token=" + urlencode(token) + "&channel=" + urlencode(channelID) + "&text=" + urlencode(text) + "&username=" + urlencode(username); print(postData); S data = doPost(postData, url); Map map = jsonDecodeMap(data); lastAnswerTimestamp = (S) map.get("ts"); lastAnswerScore = 0; print("postAnswer: " + data); ret lastAnswerTimestamp; } static S getNewQuestions_lastSeen; static L getNewQuestions() { // Get 150 on first call (history before bot start) // and 1000 thereafter (just grab anything new) int limit = getNewQuestions_lastSeen == null ? 150 : 1000; L msgs = slackReadChannel(channelID, token, limit, getNewQuestions_lastSeen); if (!msgs.isEmpty()) getNewQuestions_lastSeen = last(msgs).ts; int i = indexOfLastBotAnswer(msgs); if (i >= 0) { print("Last bot answer at " + (i+1) + "/" + l(msgs) + ": " + structure(msgs.get(i))); // update variables lastAnswer = msgs.get(i).text; for (int j = 0; j < i; j++) { SlackMsg msg = msgs.get(j); S userName = getUserName(msg.user); //if (userName != null) lastPostByUser.put(userName, msg.text); } msgs = subList(msgs, i+1); } ret msgs; } static int indexOfLastBotAnswer(L msgs) { for (int i = l(msgs)-1; i >= 0; i--) if (msgs.get(i).botName != null) ret i; ret -1; } static synchronized S answer(S s) { new Matches m; if (match("reload", s)) { reload(); ret l(bots) + "/" + l(botIDs) + " bots reloaded."; } if (match("reload *", s, m)) { reload(m.unq(0)); ret "Bot " + formatSnippetID(m.unq(0)) + " reloaded."; } if (match("list bots", s)) { S answer = "Active bots: " + structure(activeBots); L inactiveBots = diff(botIDs, activeBots); if (!inactiveBots.isEmpty()) answer += ". Inactive bots: " + structure(inactiveBots); ret answer; } if (match("test", s)) ret "blah!"; ret callStaticAnswerMethod(bots, s); } static void recordFeedback(S probableQuestion, S answer, S answerTime, S feedback, int score) { Map data = litmap("realtime", now(), "question", probableQuestion, "answer", answer, "answertime", answerTime, "feedback", feedback, "score", score); print("Recording feedback: " + data); logQuoted(new File(programDir(), "feedback-log"), structure(data)); logQuoted(new File(programDir(), "memory-log"), structure(data)); } static void reload() { botIDs = or((L) loadVariableDefinition("botIDs"), botIDs); cleanUp(bots); activeBots = new ArrayList(); botsByID.clear(); for (S botID : botIDs) pcall { loadBot(botID); } } static void reload(S botID) { S parsedID = "" + parseSnippetID(botID); Class bot = botsByID.get(parsedID); if (bot == null) ret; cleanUp(bot); botsByID.remove(parsedID); activeBots.remove(bot); loadBot(botID); } static void loadBot(S botID) { print("Loading bot: " + botID); Class c = run(botID); bots.add(c); activeBots.add(botID); // only if loading doesn't fail botsByID.put("" + parseSnippetID(botID), c); print("Loaded bot: " + botID + ", bots now: " + l(activeBots)); } static S getUserName(S userID) { if (userID == null) ret null; S userName = userNames.get(userID); if (userName == null) pcall { userName = (S) slackGetUserInfo(token, userID).get("name"); userNames.put(userID, userName); } ret userName; } static NanoHTTPD.Response serve(S uri, NanoHTTPD.Method method, Map header, Map parms, Map files) { print("Serving HTTP " + quote(uri)); ++hitCount; saveLocally("hitCount"); uri = dropPrefixMandatory("/", uri); if (eq(uri, "")) ret serveHomePage(); if (eq(uri, "favicon.ico")) ret serve404(); int i = uri.indexOf('/'); S firstPart = i >= 0 ? uri.substring(0, i) : uri; S rest = i >= 0 ? substr(uri, i) : "/"; // rest always starts with "/" ret serveBot(firstPart, rest); } static NanoHTTPD.Response serveBot(S botID, S subUri) { Class bot = botsByID.get("" + parseSnippetID(botID)); boolean raw = subUri.equals("/raw") || subUri.startsWith("/raw/"); if (raw) { subUri = substr(subUri, "/raw".length()); if (subUri.length() == 0) subUri = "/"; } if (bot != null) { S botHtml = callHtmlMethod(bot, subUri); if (raw) ret serveHTML(unnull(botHtml)); if (botHtml == null) botHtml = "Bot has no HTML output."; S title = "TinyBrain Bot " + formatSnippetID(botID); S html = "" + title + "" + "

Bot " + formatSnippetID(botID) + "

\n" + botHtml + "\n"; ret serveHTML(html); } ret serve404(); } static NanoHTTPD.Response serveHomePage() { new StringBuilder buf; buf.append(""); buf.append("TinyBrain Chat Bots"); buf.append(""); buf.append("

TinyBrain Bots

"); buf.append("
    "); for (S botID : activeBots) { botID = formatSnippetID(botID); S parsedID = "" + parseSnippetID(botID); S title = "?"; pcall { title = getSnippetTitle_overBot(botID); } S name = botID + " - " + htmlencode(title); buf.append("
  • " + name + " ("); buf.append("source)
  • "); } buf.append("
"); buf.append("

Hit count: " + hitCount + "

"); buf.append("

Last interactions:

"); int maxInteractionsToPrint = 10; for (int i = l(history)-1; i >= 0 && i >= l(history)-maxInteractionsToPrint; i--) { Map m = history.get(i); buf.append("

" + htmlencode(m.get("question")) + "
    " + htmlencode(m.get("answer")) + "

"); } buf.append(""); buf.append(""); ret serveHTML(buf); } static S getSnippetTitle_overBot(S snippetID) { startBot("Snippet Title Bot", "#1001747"); // Use local version //S s = sendToLocalBot(getVMPort() + "/Snippet Title Bot", "what is the title of snippet *", snippetID); // Use bot VM version S s = sendToLocalBot_cached("Snippet Title Bot", "what is the title of snippet *", snippetID); new Matches m; if (match("The title of snippet * is *.", s, m)) ret m.unq(1); throw fail("Bot said: " + s); } static void readLog() { for (S s : scanLog("#1001915", "memory-log")) pcall { history.add((Map) safeUnstructure(s)); } print(l(history) + " history entries."); }