Conversation AI

A simple script to handle simple conversation trees. Example conversation tree notecard is below the script.

 
 
///////////////////////
// User Variables //
///////////////////////

// The name of the note with the conversation tree.
string note = "conversation";

// The following variable determines if conversations
// Are exclusive to the person who initiates them.
integer exclusive = TRUE; 

//////////////////////////
// System Variables //
//////////////////////////

// List layout
// How a line and conversation are arranged
integer STRIDE = 5;     // How many parts to a line
integer STATE = 0;      // The state of this command
integer NEXTSTATE = 1;  // The next state after this command
integer TRIGGER = 2;    // The trigger string
integer MESSAGE = 3;    // The text message
integer COMMAND = 4;    // The link command

// The current section of the notecard.
list currTriggers;
list currMessages;
list currNextStates;
list currCommands;

// The entire contents of the notecard.  Could get large.
// state, goto, trigger, message, command
list conversation;

// Others
list tempL;
integer i;
integer j;
integer l;
integer max;
key k;
key user = NULL_KEY;

/////////////////
// Functions //
/////////////////

// Replaces all occurances of find in src with rep
string replace(string src, string find, string rep)
{
    // Save our selves some work in an easy double edge case
    if (src == find) return rep;
    
    // Prevent infinite loops when find is in rep
    else if (llSubStringIndex(rep,find) != -1) return src;
    
    // Look for find in src
    l = llSubStringIndex(src,find);
    
    // While there are still find in src
    while (l != -1)
    {
        // It is at the begning of src
        if (l == 0)
            src = rep + llGetSubString(src,llStringLength(find),- 1);

        // It is at the end of src
        else if (l == llStringLength(src) - llStringLength(find))
            src = llGetSubString(src,0,0 - llStringLength(find) - 1) + rep;
        
        // It must be in the middle of src
        else
            src = llGetSubString(src,0,l - 1) + rep + 
                    llGetSubString(src,l + llStringLength(find),-1);
        
        // See if there are more
        l = llSubStringIndex(src,find);
    }
    
    return src;
}

// Set the conversation state.
// Note this is a 'fake' state, not an LSL state.
// Conversation is assumed to be sorted by STATE
integer setState(integer s)
{
    // Grab a list of the states
    tempL = llList2ListStrided(conversation,0,-1,STRIDE);
    
    // Find the first occurance of the state s
    i = llListFindList(tempL,[(string)s]);
    
    // If the state doesn't exist, go to the zero state
    if (i == -1)
    {
        i = 0;
        s = 0;
    }
    
    // Find the first occurance of the state s + 1
    j = llListFindList(tempL,[(string)(s + 1)]);
    
    // If there is no next state, must be at the end of the list
    if (j == -1) j = 0;
    // We want through the last of s, which is right before s + 1
    j = j - 1;

    // Convert i and j to locations in conversation
    i = i * STRIDE;
    j = j * STRIDE;
    
    // Pull the chunks out of conversation into their own sections.
    currTriggers = llList2ListStrided(
                        llList2List(conversation,
                                    i + TRIGGER,
                                    j + TRIGGER),
                        0,-1,STRIDE);
                        
    currNextStates = llList2ListStrided(
                        llList2List(conversation,
                                    i + NEXTSTATE,
                                    j + NEXTSTATE),
                        0,-1,STRIDE);
                        
    currMessages = llList2ListStrided(
                        llList2List(conversation,
                                    i + MESSAGE,
                                    j + MESSAGE),
                        0,-1,STRIDE);
                        
    currCommands= llList2ListStrided(
                        llList2List(conversation,
                                    i + COMMAND,
                                    j + COMMAND),
                        0,-1,STRIDE);
                        
    return s;
}

//////////////
// States //
/////////////
default
{
    state_entry()
    {
        // We listen to everything
        llListen(0,"","","");
        
        llWhisper(0,"...setting up system...");
        i = 0;
        conversation = [];
        k = llGetNotecardLine(note,i++);
    }

    listen(integer chan, string name, key id, string mes)
    {
        if ( (exclusive && ( user == NULL_KEY || user == id)) || !exclusive)
        {
            // Cycle through every current trigger
            max = llGetListLength(currTriggers);
            for (i=0;i

 

 


Here is an example conversation notecard. It goes on a notecard named "conversation" in the same object as the above script. This note will cause a link message to be sent at the begning and end of the conversation - for simple conversations this is totally unnecesary.

 

// State, Goto State, Trigger, Message
// This is a response that will never leave the 0 level, you can ask this as much as you want
0-=-0-=-Are you a computer?-=-Yes, I am!

// This starts an actual conversation, and moves to the next conversation level
0-=-1-=-Hello-=-Hi, -@-!-=-StartingConversation

// This is the next level of conversation
// The AI will only respond to these after someone says 'Hello'
1-=-1-=-What color is the sky?-=-Why, -@-, do you know, I think it's blue!

// This will end the conversation by going back to the 0 level
1-=-0-=-Bye-=-Ok, -@-!-=-EndingConversation
Another approach to scripting natural language AI and natural language interaction with commands is through an AIML like markup language. An example of such a markup would be lindenAIML (seeLibraryLindenAIML).

 

 

Bartender Script

A simple script that all you need to do is drop in the drinks and customers can click on the bartender to have a menu pop up.

 

//
// BETLOG Hax
// for Aley Arai AEST: 20080328 0645 [SLT:  20080327 1345]
// Written to allow easier use - just dump drink objects into inv and you are done.
// No more editing script drink lists.
//=========================================================================
// ---LICENCE START---
// http://creativecommons.org/licenses/by-sa/3.0/
// ie: Attribution licence:
//   Give me credit by leaving it in the script I created.
//   Supply my original script with your modified version.
//   Refer to the wiki URL from which you copied this script.
//      https://wiki.secondlife.com/wiki/Bartender
// ---LICENCE END---
//=========================================================================
 
//----------------------------------
// SHARED CONFIGURATION
//----------------------------------
// CONFIGURATION
//
//OPTIONAL debug feedback - 0=off, 1=Channel & Noise info, 2=all debug
integer gDebug              = 
                                //0;
                                1;
                                //2;
//
//number of invalid messages received before re-randomizing channel
integer gChannelNoiseMax    = 5; 
//
// message that appears on the dialog
string  gDlgMessage         = "\n\t\n\t\n\t\t\tMay I offer you a drink?";
//
// Optional chat responses
// leave BOTH of these fields blank ( ""; ) to NOT speak a message
// leave one OR the other blank to alter the form of the message
//recipient name is auto-inserted between these two
string  gMsgResponseA       = "Thanks";
// target name ends up here
string  gMsgResponseB       = ", enjoy your drink. :)";
//----------------------------------
// CORE CODE
integer gListenHandle       = 0;
integer gDlgLimiter         = 0;
integer gDlgChannel         = -5746547; //failsafe only
list    gDrinksList         = [];
integer gDrinksCount        = 0;
integer gChannelNoise       = 0;
//----------------------------------
f_dialogMenu(key id, list dlgButtons)
{   llDialog(id, gDlgMessage, dlgButtons, gDlgChannel);
    if (gDebug > 1)
    {   llOwnerSay("***OFFERING DIALOG:***"
            +"\nname: "+llKey2Name(id)
            +"\nid: "+(string)id
            +"\ngDlgMessage: "+gDlgMessage
            +"\ndlgButtons: "+llList2CSV(dlgButtons)
            +"\ngDlgChannel: "+(string)gDlgChannel
            +"\n***************"
        );
    }
}
//-----------------------
f_dialogNullify()
{   gDlgLimiter = 0;    
}
//-----------------------
f_randomChannel()
{   
//a static channel is more reliable where many people may request a drink while another is still considering the choices
//gChannelNoise will trigger auto-re-randomization of this static channel if too much invalid message activity is detected
    if (gListenHandle)
        llListenRemove(gListenHandle);
    gDlgChannel = (integer)(llFrand(-0x7FFFFFFF)-1);
    gListenHandle = llListen(gDlgChannel, "", "", ""); 
    if (gDebug)
        llOwnerSay("gDlgChannel: "+(string)gDlgChannel);
}
//-----------------------
f_assessInventory()
{   integer    i;
    integer    n = llGetInventoryNumber(INVENTORY_OBJECT);
    gDrinksList = [];
    for(i=0; i 1)
        llOwnerSay("gDrinksCount: "+(string)gDrinksCount+" - gDrinksList: "+llList2CSV(gDrinksList));        
}
//-----------------------
default
{   on_rez(integer start_param)
    {   llResetScript();
    }
    changed(integer change)
    {   if (change & CHANGED_INVENTORY)
        {   llSetText("RE-EVALUATING\ninventory.", <0.0, 1.0, 0.0>,1.0);            
            //so its not being called too much and slowing down inv changes            
            llSetTimerEvent(8.0);
        }
        if (change & CHANGED_OWNER)
            llResetScript();
    }
    timer()
    {   llSetTimerEvent(0.0);
        llSetText("", <1 .0, 1.0, 1.0>,1.0);
        f_assessInventory();        
    }
    state_entry()
    {   f_randomChannel();
        f_assessInventory();
    }
    touch_end(integer num_detected)
    {   gDlgLimiter += 1;
        if (gDlgLimiter == 1)
        {   key av = llDetectedKey(0);
            f_dialogMenu(av, gDrinksList);            
        }
        else if (gDlgLimiter >= 2) //3)
            f_dialogNullify();
    }
    listen(integer channel, string name, key id, string message)
    {   integer index = llListFindList(gDrinksList, [message]);
        if (index == -1)
        {   gChannelNoise++;
            if (gDebug)
                llOwnerSay("FYI - INVALID MESSAGE DETECTED ON THIS CHANNEL:\ngDlgChannel: "+(string)gDlgChannel+"\ngChannelNoise/gChannelNoiseMax = "+(string)gChannelNoise+"/"+(string)gChannelNoiseMax+"\nFrom: "+name+" [key: "+(string)id+"]");
            if (gChannelNoise >= gChannelNoiseMax)
                f_randomChannel();
            return;
        }
        string drink = llGetInventoryName(INVENTORY_OBJECT, index);
        if (drink != "")
        {   f_dialogNullify();            
            if ( (gMsgResponseA != "") || (gMsgResponseB != "") )
                llSay(0, gMsgResponseA+" "+name+" "+gMsgResponseB);
            llGiveInventory(id, drink);
            if (gDebug > 1)
                llOwnerSay("drink: "+drink+" was given to: "+name+" [key: "+(string)id+"]");
        }
        else
        {   // SHOULD never happen
            llOwnerSay("ERROR: The bar has no drinks!!!..or somthing else bad happened..");
        }
    }
}