User:Decimus Schomer/Scripts/Chatbot
Decimus' user page | Decimus' talk page | Decimus' scripts | Decimus' script libraries | Decimus' projects
Main scripts page | Toggling Rotate script | UUID-getter scripts | Texture changer | Channel spier | Chatbot | Jump slab | Emailer | Fractal viewer | Grammar analyser | SPD viewer
Contents
About
This is a relatively simple chatbot script.
Script
// Chatbot - A (relatively) simple chatbot script // Copyright (C) 2007 Decimus Schomer // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. string VERSION = "1.1"; string DEF_REP_UNK = "Sorry, I don't understand you."; string DEFAULT_REP_UNK; integer line; integer lines; string CARDNAME; integer MAXQUEUE = 60; list cur_match; list card_starts; integer cstart; list matches; integer st; integer st2; string message; string ournam; list ournams; string ownnam; list ownnams; string spknam; list spknams; list rep_unk; string tab; string ht = "0123456789abcdef"; string list2str(list l) { string raw = llDumpList2String(l, ""); integer i = 1; string ch; while (i < 256) { ch = llGetSubString(tab, i, i); if (~llSubStringIndex(raw, ch)) i++; else return ch + llDumpList2String(l, ch); } return ""; } list str2list(string s) { return llParseStringKeepNulls(llGetSubString(s, 1, -1), [llGetSubString(s, 0, 0)], []); } loadcard(string name) { CARDNAME = name; line = -1; llGetNumberOfNotecardLines(name); } clear() { matches = []; card_starts = []; rep_unk = [DEFAULT_REP_UNK]; } init() { integer i; tab = " "; for (i = 1; i < 256; i++) { tab += llUnescapeURL("%" + llGetSubString(ht, i >> 4, i >> 4) + llGetSubString(ht, i & 15, i & 15)); } DEFAULT_REP_UNK = list2str(llParseStringKeepNulls(DEF_REP_UNK, ["%"], [])); ownnam = llKey2Name(llGetOwner()); ownnams = llParseString2List(ownnam, [" "], []); ournam = llGetObjectName(); ournams = llParseString2List(ournam, [" "], []); clear(); } string _get_arg(list args, string sv, integer recursive) { integer num_args = llGetListLength(args); integer iv = (integer)sv; integer idx; integer i; string v; list l; list resps; list nargs; if (llGetSubString(sv, 0, 5) == "SPKNAM") { if (llStringLength(sv) <= 6) return spknam; else { iv = (integer)llGetSubString(sv, 6, -1); return llList2String(spknams, iv); } } else if (llGetSubString(sv, 0, 5) == "OURNAM") { if (llStringLength(sv) <= 6) return ournam; else { iv = (integer)llGetSubString(sv, 6, -1); return llList2String(ournams, iv); } } else if (llGetSubString(sv, 0, 5) == "OWNNAM") { if (llStringLength(sv) <= 6) return ownnam; else { iv = (integer)llGetSubString(sv, 6, -1); return llList2String(ownnams, iv); } } else if (sv == "VERNUM") { return VERSION; } else if (llGetSubString(sv, 0, 5) == "EQUIV ") { if (llStringLength(sv) < 7) llSay(0, "Invalid EQUIV command!"); else { sv = llGetSubString(sv, 6, -1); idx = llSubStringIndex(sv, " "); nargs = []; if ((idx != -1) && (idx != llStringLength(sv) - 1)) { l = llParseStringKeepNulls(llGetSubString(sv, idx+1, -1), ["`"], []); for (i = 0; i < llGetListLength(l); i++) { nargs += [_get_arg(args, llList2String(l, i), 1)]; } } resps = str2list(llList2String(matches, (2 * (integer)sv) + cstart + 1)); return _say_msg(str2list(llList2String(resps, (integer)llFrand(llGetListLength(resps)))), nargs); } } else if ((string)iv == sv) { if (iv >= num_args) { llSay(0, "Request for argument " + sv + ", but only " + (string)num_args + " arguments exist!"); return ""; } else return llList2String(args, iv); } else if (sv == "*") return message; if (! recursive) { llSay(0, "Invalid replacement: %" + sv + "%"); return ""; } return sv; } string _say_msg(list msg, list args) { integer i; string sv; integer iv; for (i = 1; i < llGetListLength(msg); i += 2) { sv = llToUpper(llList2String(msg, i)); iv = (integer)sv; msg = llListReplaceList(msg, [_get_arg(args, sv, 0)], i, i); } return llDumpList2String(msg, ""); } say_msg(list msg, list args) { string s = _say_msg(msg, args); list l = llParseString2List(s, [], [".", "!", "?"]); string m; integer i; integer j; string ch; for (i = 0; i < llGetListLength(l); i++) { m = llList2String(l, i); for (j = 0; j < llStringLength(m); j++) { ch = llGetSubString(m, j, j); if (llToLower(ch) != llToUpper(ch)) { ch = llToUpper(ch); if (j == 0) m = ch + llGetSubString(m, 1, -1); else if (j == llStringLength(m) - 1) m = llGetSubString(m, 0, -2) + ch; else m = llGetSubString(m, 0, j-1) + ch + llGetSubString(m, j+1, -1); l = llListReplaceList(l, [m], i, i); j = llStringLength(m); } } } m = llDumpList2String(l, ""); m = llToUpper(llGetSubString(m, 0, 0)) + llGetSubString(m, 1, -1); llSay(0, m); } list match(list strlst, list strlst_nl, list patt) { integer idx = 0; integer st = 0; integer i; list asterices = []; for (i = 0; i < llGetListLength(patt); i++) { string ps = llList2String(patt, i); if (st == 1) { idx = llListFindList(strlst, [ps]); if (idx == -1) return [0]; string dat = ""; if (idx > 0) dat = llDumpList2String(llList2List(strlst_nl, 0, idx - 1), " "); if (idx == llGetListLength(strlst) - 1) strlst = []; asterices += [dat]; strlst = llList2List(strlst, idx+1, -1); strlst_nl = llList2List(strlst_nl, idx+1, -1); st = 0; } else if (ps == "*") { if (strlst != []) st = 1; } else { if (llList2String(strlst, 0) != ps) return [0]; if (idx == llGetListLength(strlst) - 1) strlst = []; strlst = llList2List(strlst, idx+1, -1); strlst_nl = llList2List(strlst_nl, idx+1, -1); } } if ((strlst != []) && (llList2String(patt, -1) != "*")) return [0]; else if (strlst != []) asterices += [llDumpList2String(llList2List(strlst_nl, 0, -1), " ")]; else if (llList2String(patt, -1) != "*") asterices += [""]; return asterices; } matchany() { list strlst = llParseString2List(llToLower(message), [" "], []); list strlst2 = llParseString2List(message, [" "], []); integer i; integer j; list patts; list reslsts; list asterices; cstart = -1; integer ncstart = 0; integer ncidx = 0; for (i = 0; i < llGetListLength(matches); i += 2) { patts = str2list(llList2String(matches, i)); reslsts = str2list(llList2String(matches, i+1)); if (ncstart >= cstart) { cstart = ncstart; ncstart = llList2Integer(card_starts, ncidx); ++ncidx; } for (j = 0; j < llGetListLength(patts); j++) { asterices = match(strlst, strlst2, str2list(llList2String(patts, j))); if (llGetListEntryType(asterices, 0) == TYPE_STRING) { say_msg(str2list(llList2String(reslsts, (integer)llFrand(llGetListLength(reslsts)))), asterices); return; } } } if (rep_unk != []) say_msg(str2list(llList2String(rep_unk, (integer)llFrand(llGetListLength(rep_unk)))), []); } string compacted(string s) { return llDumpList2String(llParseString2List(s, [" "], []), " "); } integer proc_opt(string data) { string arg; string low = llToLower(data); if (llGetSubString(low, 0, 6) == "rep-unk") { if (llStringLength(data) <= 8) return -2; else { arg = compacted(llGetSubString(data, 7, -1)); if (arg == "") return -2; else { rep_unk += [list2str(llParseStringKeepNulls(arg, ["%"], []))]; } } } else if (llGetSubString(low, 0, 10) == "clr-rep-unk") { rep_unk = []; } else if (llGetSubString(low, 0, 12) == "reset-rep-unk") { rep_unk = [DEFAULT_REP_UNK]; } else { return -1; } return 0; } proc_line(string data) { list split = llParseStringKeepNulls(data, ["||"], []); list l = []; integer len = llGetListLength(split); integer i; string s; string ls; integer f = 0; for (i = 0; i < len; i++) { s = llList2String(split, i); while (llGetSubString(s, -1, -1) == "\\") { ++i; if ((i == len) || ((i == len-1) && (llList2String(split, -1) == ""))) { if (i != len) ++i; s = llGetSubString(s, 0, -2) + "||"; f = 1; } else s = llGetSubString(s, 0, -2) + "||" + llList2String(split, i); } if (! st2) s = list2str(llParseString2List(s, [" "], [])); else s = list2str(llParseStringKeepNulls(compacted(s), ["%"], [])); l += [s]; } if ((f) || (llGetSubString(data, -2, -1) != "||")) { matches += [list2str(cur_match + l)]; st2 = st2 ^ 1; cur_match = []; } else cur_match += l; } dump_all() { integer i; integer j; list l; list l2; l2 = []; for (i = 0; i < llGetListLength(rep_unk); i++) { l = str2list(llList2String(rep_unk, i)); l2 += [llDumpList2String(l, "%")]; } llSay(0, "rep-unk: ['" + llDumpList2String(l2, "' || '") + "']"); for (i = 0; i < llGetListLength(matches); i++) { l = str2list(llList2String(matches, i)); l2 = []; if (! (i % 2)) { for (j = 0; j < llGetListLength(l); j++) { l2 += [llDumpList2String(str2list(llList2String(l, j)), " ")]; } } else { for (j = 0; j < llGetListLength(l); j++) { l2 += [llDumpList2String(str2list(llList2String(l, j)), "%")]; } } llSay(0, llDumpList2String(l2, " || ")); } } default { state_entry() { init(); llListen(0, "", "", ""); llListen(1, "", llGetOwner(), ""); } listen(integer channel, string name, key id, string msg) { integer i; integer j; list l; list l2; list l3; list l4; integer ll; string s; message = msg; msg = llToLower(msg); if (channel == 0) { spknam = name; spknams = llParseString2List(name, [" "], []); matchany(); } else { if (msg == "dump") dump_all(); else if (msg == "clear") clear(); else if (llGetSubString(msg, 0, 4) == "load ") loadcard(llGetSubString(msg, 5, -1)); } } dataserver(key qid, string data) { integer i; integer l; if (line == -1) { integer nmatches = llGetListLength(matches); if (nmatches > llList2Integer(card_starts, -1)) card_starts += [nmatches]; lines = (integer)data; llSay(0, "Processing notecard '" + CARDNAME + "'. Line count: " + data); line = 0; st = 0; for (i = 0; i < MAXQUEUE; i++) { l = line + i; if (l >= lines) return; llSetText("Queueing: " + (string)(l + 1) + "/" + (string)lines + ".", <0, 0, 1>, 10); llGetNotecardLine(CARDNAME, l); } } else { if (data) { llSetText("Processing: " + (string)(line + 1) + "/" + (string)lines + ".", <0, 0, 1>, 10); if (! st) { if (llGetSubString(data, 0, 0) != "%") { proc_line(data); st = 1; } else { integer i = proc_opt(llGetSubString(data, 1, -1)); if (i == -1) llSay(0, "Unknown option on line " + (string)line + ": '" + data + "'"); else if (i == -2) llSay(0, "Missing argument to option on line " + (string)line + "."); } } else { proc_line(data); } ++line; if (line == lines) { if (llGetListLength(matches) % 2) { llSay(0, "Missing reply for final entry!"); matches = llDeleteSubList(matches, -1, -1); } llSetText("Ready.", <0, 0, 1>, 10); llSay(0, "Notecard '" + CARDNAME + "' loaded."); } } if (! (line % MAXQUEUE)) { for (i = 0; i < MAXQUEUE; i++) { l = line + i; if (l >= lines) return; llSetText("Queueing: " + (string)(l + 1) + "/" + (string)lines + ".", <0, 0, 1>, 10); llGetNotecardLine(CARDNAME, l); } } } } }
Usage
To use this script, you simply have to put it in an object along with some content notecards, then tell it to load each of the notecards (remembering that the first notecard loaded overrides any notecards loaded later!) The notecards are reasonably simple: A set of zero or more option lines (see 'options' for details), followed by a series of input/reply groups (see 'input and replies' for more details)
Commands
You may say any of these commands on channel 1 (prefix it with '/1'). Only the owner is allowed to issue these commands to the bot:
- load notecard name (notecard name being the name of the notecard) - load the notecard named by notecard name
- clear - Reset the bot's memory.
- dump - Dump the bot's memory as speech. This tends to produce large amounts of output if it has been given lots of input!
Options
Option lines start with '%'s and a name, followed by an argument (to the end of the line) if the option requires one. The following options are currently supported:
- 'rep-unk', which takes an argument and adds it to the list of default replies which the bot chooses from when what you say doesn't match any of its entries
- 'clr-rep-unk', which clears the list of default replies utterly
- 'reset-rep-unk', which resets the default reply list (to the default of just "Sorry, I don't understand you.")
Input and replies
Inputs and replies may come in groups - you can have multiple inputs associated with the same reply or group of replies (and multiple replies associated with the same input). To make a group of inputs or replies, separate each alternative with a double bar ('||', without the quotes). Whitespace between entries, including newlines, is ignored. (though the double bar currently has to be the last pair of characters on the line; it can't have spaces or anything after it)
Also, if you want to include a literal '||' within the text, you may do so by putting '\||'; the slash will not be included. If you want to end the alternative with a backslash rather than escaping the separator, simply put a space between the slash and the double bar.
Input
Each input line may include asterices to match any amount of text up to the next word ('* *' gets all the text up to the next asterix, not any number of words split in some arbitrary manner)
Replies
Each output can include any of the following tokens (it's case-insensitive, so 'abc' is equivalent to 'ABC'), surrounded immediately (no whitespace!) by '%'s:
- SPKNAM - this produces the name of the person who just spoke
- SPKNAMn (where n is a number, not a literal 'n') - get the nth name of the speaker (this is 0-based). Normal avatars have just two names, but objects can have more.
- OURNAM/OURNAMn - same as SPKNAM and SPKNAMn, but uses the bot's own name instead.
- OWNNAM/OWNNAMn - same as SPKNAM and SPKNAMn, but uses the bot's owner's name instead.
- n (where n is either a number or an asterix) - if n is a number, get the text matched by the nth asterix in the input sentence (0-based; if n is greater than the number of the final asterix, an error is said). If n is an asterix, retrieve the whole input text (including text which wasn't matched by an asterix)
- EQUIV n args... (where n is a number and args... is a list of either one of these tokens (not including '%'s, and only single-argument EQUIVs are allowed for obvious reasons) or normal text, separated by angled quotes ('`')) - Get the output of the nth (0-based) reply in this notecard, replacing the arguments which would normally come from '*'s in the input with args.... See the sample card below for an example - it uses 'greet *' to allow you to make it greet someone, plus if you say 'hi' or anything similar to it, it acts as if you'd said 'greet <your name>'
Sample notecard
This notecard provides an example of how the chatbot can be used. It makes the bot respond to 'say <something>' with whatever <something> was and makes it answer correctly to the questions 'Who are you?' and 'Who owns you?' (as well as some variants):
%clr-rep-unk say * to * %0%, %1% say * %0% greet * Hi, %0%! || Hello, %0%. || Hi! who is your owner || who is your owner? || who owns you || who owns you? My owner is %OWNNAM% || I am owned by %OWNNAM% hello || hi || hey || hello. || hi. || hey. || hello! || hi! || hey! %EQUIV 2 SPKNAM% what is your name || what is your name? || who are you || who are you? I am %OURNAM%, version %VERNUM% what is my name || what is my name? || who am i || who am i? Your name is %SPKNAM0%.