Racter

From Second Life Wiki
Jump to: navigation, search
KBnote.png Note: Due to licensing issues, all contributions to this wiki have stopped and the articles that we posted are just being maintained. This is one of the projects that has gone further and for updates you are cordially invited to the project page on our wiki.

Created by Kira Komarov.

Introduction

Racter is a cynical nonsense speaking chatterbot modeled as a tribute to the Amiga's classic Racter program but with a little more brain by taking concepts from Eliza chatterbots. Racter allows multiple brain-files (notecards in SecondLife) to be added in order to expand the palette of possible keywords and the replies that the bot will give. In order to not clutter up the main chat, Racter is programmed to only answer to sentences that include its name. However, Racter has a feature which allows it to talk freely with a certain configurable probability.

Setting Up Racter

To set Racter up, create a notecard called Racter_1 and fill it with the text from here or here or here. Then, create another notecard called Racter_2 and paste this text or this one or this one in it. These two notecards will act as Racter's brain and later on I will explain how you can create or customize them yourself without depending on this default text. You can now create a prim, name it Racter and drop these two notecards you just created in the prim.

Next, create a script called Racter (the name is important, I will explain this later on) and paste the LSL code on this page. After that, drop this script in the same prim where you put the two notecards. Racter will inform you that it is reading the notecards and after a while it should be ready and you can talk to it on the main channel.

Changing the Bot's Name

In order to change the chatterbot's name from Racter to something else, you have to do the following:

  • Change the script name to the new bot's name. For example, if you want the bot to be called Eliza, simply rename the script to Eliza.
  • Change the notecards' names. For example, if you want the bot to be called Eliza, rename the notecard Racter_1 to Eliza_1 and the notecard Racter_2 to Eliza_2 and so on for as many cards as you have.
  • Since the prim will answer with its own name, it might be wise to change the name of the prim that the bot is in to the bot's name. For example, if the bot is called Eliza, change the prim name to Eliza.

Following the example, at the end, the prim's inventory would look like this:

Eliza
Eliza_1
Eliza_2

where Eliza is the LSL script below, and Eliza_1 and Eliza_2 are the notecards.

Answering the Main Chat

The bot will answer sentences on the main chat if:

  • it sees its own name in a sentence in the main chat.
  • randomly, depending on the probability setting.

For probabilities, as a factory setting, Racter is configured to answer all the sentences it sees on the main chat, regardless whether they contain the bot's name or not. This might prove to be quite spammy if there are a lot of people speaking on the main chat. In order to change that, you have to locate the following line:

integer SPEAK_FREELY = 100;

and change it to a lower percentage. This is roughly the probability with which the bot will answer a sentence it sees on the main chat. For example, if you want the bot to be silent, you can change this to:

integer SPEAK_FREELY = 0;

and the bot will only answer sentences if it sees its own name in the sentence.

The Brain Files / Notecards

The most important part of Racter, are the notecards which constitute the bot's brains. The syntax of the lines in the notecards is the following:

<keyword>/<keyword>/<...>#<answer>/<answer>/<answer>/<...>#<weight>

where the keywords can be single words or phrases, the answers can be words or phrases and the weight is an integer.

The way that Racter works, is that it analyses a sentence and chooses a random answer from the pool with the biggest weight. This might seem complex, but it is easy to understand. Let us take an example. Suppose that you would say on the main chat:

Kira Komarov: hello racter, how are you today?

and suppose that there is only one notecard containing:

hello#hi there!/how do you do?#1
how are you#all's fine and dandy/just fine#2

Racter will pick apart your sentence, it will see "hello" and it will see "how are you" since they are both in the notecard. Then it will select an answer based on the weight. In this case, it does see "hello" but it has a weight of "1" and it will also see "how are you" which has a bigger weight of "2". Because of that, it will answer with either "all's fine and dandy" or "just fine" randomly. Here is a transcript:

Kira Komarov: hello racter, how are you today?
Racter: all's fine and dandy

Whenever you edit a notecard/brain file for Racter, you should consider that some words take precedence over others. In the example above, we could have added another line:

you#what about me?#5

but this would not be plausible since when somebody asks you a question or tells you something, you would seldom choose to craft your reply around the word "you". You would most likely comment something about "today", or since the question really is "how are you", you would most likely answer to that.

Racter supports any number of brain files (up the script memory limit). You can add additional notecards if you like, but make sure that they follow the name pattern. Again, if the bot is to be called Eliza, your notecards will be named Eliza_1, Eliza_2, Eliza_3 and so on.

Notecard Limitations

When you add lines to Racter, keep it mind that (regrettably) LSL only allows us to read the first 255 bytes from a line. In other words, if the line is too long, it will not be read properly and will most likely not even register with the bot. I have created a way around that. Suppose you wanted to include lots of replies for a keyword, and suppose that the line looks something like:

memory#do you have a good memory?/my memory often fails me/it is hard to remember something if your memory is fleeting/we all have our time machines... dreams that carry us forward and memories that take us back/scientists say that our brains will never fill up completely with information#1

and suppose that you have lots of responses in the response slot: "do you have a good memory?/my memory often fails me/it is hard to remember something if your memory is fleeting/we all have our time machines... dreams that carry us forward and memories that take us back/scientists say that our brains will never fill up completely with information" which go over 255 bytes in length. In that case, simply create two lines with the same keyword. For example, we could break that line down into:

memory#do you have a good memory?/my memory often fails me/it is hard to remember something if your memory is fleeting#1
memory#we all have our time machines... dreams that carry us forward and memories that take us back/scientists say that our brains will never fill up completely with information#1

Brain Syntax Line Limitations

The keyword part of a line in a notecard brain file, should not include the following:

  • Capital letters.
  • Punctuation marks.

However, the answers may of course include them.

Even if you would want to match a sentence that includes capital letters and punctuation marks, you should still keep the sentence that you place in the keyword part of a line clean of any punctuation marks or capital letters. Racter will however match the line even if it contains punctuation marks and capital letters. For example, if you would have a notecard line like:

i say, who's on first whats on second i don't know on third#Are you the manager?#1

You can still ask the question when you talk to Racter and include punctuation marks and capitalization. You just must not use them in the keyword section. Here is the transcript:

Kira Komarov: I say, who's on first? Whats on second? I don't know on third...
Kira the parrot: Are you the manager?

Miss Responses

Racter is meant to be a chatbot by answering with a general statement or question in order to make it a likely response. However, indifferent of the amount of notecards you add, there might be some unforeseeable events when Racter cannot match any keyword. In this case, Racter is programmed to give you a miss response. This miss response is hardcoded in the code and you might want to change that as well. Again, it is wise to chose this carefully since this response will be given when absolutely nothing matches and if you want Racter to appear real, the miss response must be something vague enough.

The miss responses can be found in the code in the form of a list:

list miss_response = [ "yeah, i guess", "mhm, i concurr", "doesn't make much sense to me", "i see", "aha, ok", "i know, right", "are you talking to me?", "uff, perhaps", "i'm not sure", "i have a headache", "my head hurts", "come again?", "i hear you", "my thoughts are cloudy", "i'm bored", "perhaps", "i see what you're saying", "hmm", "well, ok", "i get it" ];

You can add more miss responses to this list, or take away any of them. Just follow the list example and change the code accordingly.

Usage Ideas

Since Racter is based on notecard brains and thus can be programmed to respond to anything on the main chat, here are a few usage ideas that come to mind:

  • Racter could work as a shop assistant/robot. The notecards would have to be configured to answer questions asked by customers. Also, the miss responses should be changed to something less cynical. However, such an application is possible by modifying the notecards accordingly as above.
  • Two bot-chat monologue. A friend of mine Heather Iwish has been able to replicate the funny "Who's on First?" gig by Bud Abbott and Lou Costello. This basically involves two Racter bots chatting to each other on the main chat. In order to do that, she wrote the notecards so every line of the other Racter bot would match a valid response in the other Racter bot.
  • Automated greeter. For example, this could be used in roller-coaster situations where you teleport into a SIM and tens of people start greeting you. You could drop Racter in a prim (even clothing) you are wearing and alter the notecards so you greet them back without typing anything.
  • Since Racter consists of a script and notecards, you could drop this into a baby prim, for example. I have seen something like that on marketplace.

The Engine

The LSL code follows, if you have any problems or need help, feel free to contact Kira Komarov in-world.

//////////////////////////////////////////////////////////
// [K] Kira Komarov - 2011, License: GPLv3              //
// Please see: http://www.gnu.org/licenses/gpl.html     //
// for legal details, rights of fair usage and          //
// the disclaimer and warranty conditions.              //
//////////////////////////////////////////////////////////
 
/////////////////////////////////////////////////////////
// ------------------ CONFIGURATION ------------------ //
 
// How likely is it (in percentages), that Racter will 
// answer even if it is not directly spoken to.
integer SPEAK_FREELY = 100;
 
/////////////////////////////////////////////////////////
// -------------------- INTERNALS -------------------- //
key eQuery = NULL_KEY;
 
integer itra = 0;
integer itrb = 1;
integer itrc = 0;
 
list brain_cards = [];
list keywords = [];
list responses = [];
list weights = [];
 
list miss_response = [ "yeah, i guess", "mhm, i concurr", "doesn't make much sense to me", "i see", "aha, ok", "i know, right", "are you talking to me?", "uff, perhaps", "i'm not sure", "i have a headache", "my head hurts", "come again?", "i hear you", "my thoughts are cloudy", "i'm bored", "perhaps", "i see what you're saying", "hmm", "well, ok", "i get it" ];
 
readBrains() {
    brain_cards = [];
    keywords = [];
    responses = [];
    weights = [];
    itrb = 1;
    itrc = 0;
    for(itra=0; itra<llGetInventoryNumber(INVENTORY_NOTECARD); ++itra) {
        list tCard = llParseString2List(llGetInventoryName(INVENTORY_NOTECARD,itra), ["_"], [""]);
        if(llList2String(tCard, 0) != llGetScriptName() && llList2Integer(tCard, 1) != 0)
            jump continue;
        brain_cards += llGetInventoryName(INVENTORY_NOTECARD, itra);
@continue;
    }
    llOwnerSay("Reading brain files... Please wait...");
    llSetTimerEvent(1.0);
}
 
default
{
    state_entry() {
        readBrains();
    }
 
    changed(integer change) {
        if(change & CHANGED_INVENTORY) readBrains();
    }
 
    on_rez(integer start_param) {
        readBrains();
    }
 
    listen(integer chan,string name,key id,string mes) {
        list uList = llParseString2List(llToLower(mes), [" ", ",", ";", ":", ".", "?", "!", "-", "(", ")", "[", "]", "\"", "'"], [""]);
        if(SPEAK_FREELY) if((integer)llFrand((integer)(100/SPEAK_FREELY))==(integer)llFrand((integer)(100/SPEAK_FREELY))) jump free_speak;
        if(!~llListFindList(uList, (list)llToLower(llGetScriptName()))) return;
@free_speak;
        integer eScore = 0;
        integer rIndex = 0;
        for(itra=0; itra<llGetListLength(keywords); ++itra) {
            list kWords = llParseString2List(llList2String(keywords, itra), ["/"], [""]);
            for(itrb=0; itrb<llGetListLength(kWords); ++itrb) {
                if(~llSubStringIndex(llList2String(kWords,itrb), " ") && ~llSubStringIndex(llDumpList2String(uList, " "), llList2String(kWords,itrb)) && llList2Integer(weights, itra) > eScore) {
                    jump bump_score;
 
                }
                if(!~llSubStringIndex(llList2String(kWords, itrb), " ") && ~llListFindList(llParseString2List(llDumpList2String(uList, " "), [" "], [""]), (list)llList2String(kWords, itrb)) && llList2Integer(weights, itra) > eScore) {
                    jump bump_score;
 
                }
                jump continue;
@bump_score;
                eScore = llList2Integer(weights, itra);
                rIndex = itra;
@continue;
            }
        }
 
        if(!llGetListLength(miss_response) && !eScore) return;
 
        if(!eScore) {
            llSay(0, llList2String(miss_response, (integer)llFrand(llGetListLength(miss_response))));
            return;
        }
 
        list rWords = llParseString2List(llList2String(responses, rIndex), ["/"], [""]);
        llSay(0, llList2String(rWords, (integer)llFrand(llGetListLength(rWords))));
    }
 
    timer() {
        if(itrb && itrc < llGetListLength(brain_cards)) {
            llOwnerSay("Reading brain file: " + llList2String(brain_cards, itrc));
            itrb = 0;
            itra = 0;
            eQuery = llGetNotecardLine(llList2String(brain_cards, itrc), itra);
            return;
        }
 
        if(itrc == llGetListLength(brain_cards)) {
            llSetTimerEvent(0.0);
            llOwnerSay("Finished reading brain files. Racter is now available.");
            llListen(0, "", "", "");
            return;
        }
    }
 
    dataserver(key query_id,string data) {
        if (query_id != eQuery) return;
        if (data == EOF) {
            llOwnerSay("Brain file " + llList2String(brain_cards, itrc) + " read in. Moving on...");
            ++itrc;
            itrb = 1;
            return;
        }
        list split = llParseString2List(data, ["#"], [""]);
        if(llList2String(split, 0) == "" || llList2String(split, 1) == "" || llList2String(split,2) == "") jump next_line;
        if(~llListFindList(keywords, (list)llList2String(split, 0))) {
            responses = llListReplaceList(responses, (list)(llList2String(responses, llListFindList(keywords, (list)llList2String(split, 0))) + "/" + llList2String(split, 1)), llListFindList(keywords, (list)llList2String(split,0)), llListFindList(keywords, (list)llList2String(split,0)));
            jump next_line;
        }
        keywords += (list) llList2String(split, 0);
        responses += (list) llList2String(split, 1);
        weights += (list) llList2String(split, 2);
@next_line;
        eQuery = llGetNotecardLine(llList2String(brain_cards, itrc),++itra);
    }
}