Virtual Storyteller for Dummies

This document introduces how to author stories in the Virtual Storyteller, first from a technical perspective and then from a process perspective. Throughout the examples it is assumed that the reader has an installed and working version of the Virtual Storyteller on their PC.

dummies_man.gif

Authoring a story domain

Here I will describe step by step how to author a very small story domain, and in which order. I'll do this using a simple example domain, that I call the Lollipop domain (it should be in the list of story domains upon download. A variant with dialogue actions is also available under the name lollipop_dialogue). In the Lollipop domain, a little girl called Linda goes to Otto the ice cream salesman to buy an ice cream.

lollipop.jpg

Story ontology

First of all, we describe the semantics of the objects in the story. We do this using a domain specific OWL ontology. These would be objects like Ice cream, Ice cream truck, Park, Little girl, etc.

There are two basic ontologies:

The story specific ontology that one authors might import these two ontologies (use Stanford's Protege ontology editor!) and then defines new concepts as subclasses of existing concepts. (e.g., a lolli:Park is a subclass of swc:Location. It is not required to use this basic story world core ontology, but it can be useful and time saving. For our small example world we only use the Fabula ontology and define the story world from scratch.

For the objects in our lollipop world we create three basic classes: Location for locations, Object voor objects in the world, and Role for character roles of our agents. In Locations we do not make any subclasses yet, but we do for Objects. We introduce the classes Money and IceCream. This way, we will be able to add Individuals (instances) of money and ice creame later on, whereby Linda receives ice in exchange for money. For character roles, we define Kid (could also have been LittleGirl, or if necessary we can later add LittleGirl as subclass of Kid), and IceVendor (an ice cream vendor). See figure below (screenshot of a part of the OwlClasses tab in Protege). The lighter colored classes have been imported from the Fabula ontology.

lollipop-objects.png

So let's add stuff to do for our characters. As actions we define Buy (buy something), Give (give something to someone), and ToddleOffTo (go somewhere). Later we'll define the schemas (with preconditions and effects) for these, but we'll add them here already and describe them using the rdfs:comment property that every concept in the ontology has (we use this as a form of ontology documentation). ALERT! Note that there is already a big hierarchy of actions, organized under categories like TransitMove and Manipulate. This hierarchy is the result of the MSc. work of Jasper Uijlings. You can choose to use these actions (and code the corresponding action schemas in Prolog later). In a later stadum this hierarchy might disappear though.

lollipop-actions.png

Then it is time to define properties. Properties often express relationships between two Individuals of classes (and sometimes a relationship between an Individual and a value). We could introduce a property likes, that allows us to express that linda likes otto (where linda is of the type Kid and otto is of the type IceVendor. In our test domain we introduce the following relationships:

Below figure shows the properties for our lollipop ontology (screenshot of a part of the Properties tab in Protege). The lighter colored properties have been imported from the Fabula ontology.

lollipop-properties.png

Making a new domain ontology: step by step
  1. start Protégé
  2. New project -> OWL /RDF Files, click "next"
  3. Rename, e.g.,
    http://www.owl-ontologies.com/Ontology1202308737.owl becomes
    http://www.owl-ontologies.com/StoryWorldSettings/NewDomain.owl
  4. When you extend other ontologies (typically the Fabula ontology), you have to import it: use the Import ontology button ("plus" in upper left corner): "Import -> available repositories"
  5. If the ontologies are not in the list, you have to indicate in which folder they are: Add repository -> local folder -> /knowledge/ontology
  6. Select the ontology (e.g., FabulaKnowledge.owl)
  7. Now you can change the namespace prefix of the imported ontologies in Progete, if you want (e.g., change p1 in fabula)

Optional: ontology rules
Sometimes you want to make use of properties that cannot be captured in 1 triple. You can define rules for this using the rule(..) predicate. For instance, in /knowledge/prolog there are the files owl_rules.pl and swc_rules.pl; we could add somewhere in our own domain files another file called lollipop_rules.pl, if we wanted. Whenever the knowledge base is being queried (using the query/3 and query/4 predicates of schema_management.pl), the system also checks if there exists a rule/3 predicate for given fact.

An example: There is a predicate rule(S, owlr:typeOrSubType, O) which is true if S is of the type O, or of a type O', whereby O' is a subclass of O. This makes the following query a valid query: query(rule(linda, owlr:typeOrSubType, lolli:'Kid')) which applies the rule predicate.

This way you could define rules that determine if a location is reachable from another location, and use this as precondition for actions (see also: rule(L2, swcr:reachableFrom, L1) in swc_rules.pl)

The namespace prefixes swcr and owlr are 'fake'; there are no ontologies behind it. If I wanted to add rules for the lollipop domain, I would place a lollipop_rules.pl file in /knowledge/domain/lollipop/ontology/, and add rules to it using an arbitrarily chosen prefix, e.g., lollir. Once this file has been loaded, the rules can already be used in queries.

Setting

Let's form the basis for a simple story world. I'm imagining the following objects:

For this knowledge we define a Turtle (.ttl) file, named park.ttl, using a text editor and below contents (hash (#) means comment):

@prefix lolli:   <http://www.owl-ontologies.com/StoryWorldSettings/Lollipop.owl#> .
@prefix :        <http://www.owl-ontologies.com/StoryWorldSettings/Lollipop.owl#> .

# Description of the park.
# Author: Ivo Swartjes
# Date:   21 jan 2008

:oIceCreamTruck
    a   :Location ;
    :adjacent   :park_1 ;
    .
    
:oParkBench
   a   :Location ;
   .
   
:linda
    a   :Kid ;
    :has    :money_1 ;
    :at :park_1 ;
    .
    
:money_1
    a   :Money ;
    .
    
:park_1
    a   :Location ;
    :adjacent :oIceCreamTruck ;
    .        
    
:otto
    a   :IceVendor ;
    :has    :vanilla_ice_1 ;
    :at :oIceCreamTruck ;
    .       
    
:vanilla_ice_1
    a   :IceCream ;
    .    

Some explanation: the @prefix statements define shortcuts for our classes and Individuals, so that we don't have to type in the whole URI. This makes it all a bit more readable too. Where it says :linda, what is really meant is http://www.owl-ontologies.com/StoryWorldSettings/Lollipop.owl#linda. We can also write lolli:linda. This way, for the basic story world core we often use the prefix swc: and for the Fabula ontology we often use the prefix fabula:. The RDF(S) and OWL ontologies are often abbreviated to rdf:, rdfs: and owl:. In this example we only use the lollipop ontolgy in the setting definition; the term a is short for rdf:type.

Schemas

Now that we have the concepts of the story world, and a simple initial setting, we can define schemas. These are action schemas, event schemas, goal schemas, belief schemas en framing schemas (more formal definitions available if interested).

A schema is a Prolog predicate: schema(...). with one argument, namely a list. This list consists of a collection of unary predicates, like type(...), arguments(...), preconditions(...).

There is a simple schema ontology in Prolog:

% S is a schema if S is an operator schema, a goal schema, etc...
schema(S) :- 
    operator_schema(S)
;   goal_schema(S)
;   belief_schema(S)
;   thread_schema(S)
.    

% S is an operator schema (those schemas that you can "execute") 
%    if it is an action schema, a framing schema, etc...
operator_schema(S) :- 
    action_schema(S)
;   framing_schema(S)
;   event_schema(S)
.

We are going to implement lolli:Buy and lolli:ToddleOffTo as action_schemas. In the folder lollipop\schemas there are different files, you can arrange them however you see fit for a particular domain (organized by \schemas\schema.pl), but I like to organize it by giving each schema type (actions, events, goals, etc) its own file. So we create an action.pl prolog file, with two schema definitions, for the buy and toddling actions. Below, the Buy schema is explained, the ToddleOffTo schema is similar.

Buy
The schema predicate for the Buy action looks as follows (percent (%) means comment in Prolog, variables are indicated by atoms that start with a capital letter, e.g., Agens):

% Action semantics: AGENS buys PATIENS from TARGET with INSTRUMENT
action_schema([
    type(lolli:'Buy'),
    arguments([
        agens(Agens), patiens(Patiens), 
        target(Target), instrument(Instrument), 
        location(Location)
    ]),
    duration(1),
    preconditions([
        condition(true, [
            fact(Instrument, rdf:type, lolli:'Money'),
            fact(Agens, lolli:has, Instrument),
            fact(Agens, lolli:at, Location),
            fact(Target, lolli:at, Location),
            fact(Target, lolli:has, Patiens)
        ])       
    ]),
    effects([
   condition(true, [
       fact(Agens,   lolli:has, Patiens)
   ]),
        condition(false, [
            fact(Agens, lolli:has, Instrument)
        ]),
        condition(false, [
            fact(Target, lolli:has, Patiens)
        ])
    ])
]).

The type(...) predicate links the schema to the ontology class that we have defined in Protege, and the arguments(...) predicate adds semantics to all used variables. For instance: agens(X) indicates that the variable X is bound to the person that execute (is the agens of) the action. This semantics is used to be able to fill in the properties fabula:agens, fabula:patiens etc. of an action in the fabula of the simulation. Most used are agens (who does it), patiens (=direct object), target ( where from/to), instrument (=indirect object) of an action.

duration(1) indicates that the action lasts 1 time unit. If the duration(...) predicate is omitted, a standard value of 1 will be used by the system.

preconditions(...) takes as argument a list of conditions that need to be true before the action can be executed. In the Buy action the sales person (Target) must possess (has) that which is bought (Patiens). Also the buyer (Agens) must own money (Instrument), so something of the type (lolli:Money). Finally, buyer and seller must be at the same location. ALERT! there are quotes around Money in order to avoid Prolog seeing it as a variable; in Prolog anything that starts with a capital is a variable.

In the condition you can specify whether you mean that the facts should be true, or untrue. In the case of Buy there is only one condition (consisting of a list of facts) that should be true.

ALERT! It is important that ALL variables used in the schema, are also used in the preconditions of the schema, which is a requirement for the planning algorithm to work correctly (see Russell & Norvig).

effects(...) (the add- and delete-list in STRIPS terms) takes as argument a list of facts that will be true after execution of the action. After executing the Buy action, it is the case that the buyer (has) the product, and doesn't have the money anymore, and that the salesperson doesn't have the product anymore. ALERT! Effects can (for now) only contain fact(...) statements. fabula(...) statements as effects would have unclear semantics: it would mean that the effect of an action is that something has happened in the past. The system does add an (implied) fabula effect to each action, namely that the action itself has occurred. This way, the planner cna still make a plan for goals that have in their success conditions things like: "someone has bought something from me". But this fabula effect is never "executed"; instead the characters find out its fabula effects after execution, through the Plot Agent.

Goals
Goals form the main motivation for the selection of actions. Based on a goal, the agent will select actions that achieve this goal. ALERT! The distinction between actions and goals is sometimes difficult to make; in the end the actions in a plan can also be considered as subgoals of the final goal. Rule of thumb is usually: for which things in your story domain is it important whether it succeeds or not? Thats the level to specify goals for.

A goal schema is no 'operator' and therefore doesn't have a duration and effect list, but it does have:

This way, each schema carries its own schema-specific information. All schemas are loaded by schema.pl and used by (amongst others) the Partial-Order Planner.

When thinking about schemas, always think in terms of: what can happen, and under which circumstances?

For the lollipop domain we define one goal: Linda wants ice cream! We can define the goal specifically for Linda, or generalize it: a kid wants ice cream. In terms of preconditions, this means:

condition(true, [
   fact(Agens, rdf:type, lolli:'Kid')
])
condition(true, [
   fact(Patiens, rdf:type, lolli:'IceCream')
])

We add a Class HaveIce as subclass of fabula:AttainGoal in the Lollipop ontology, and specify the schema to be of this type. Together with the preconditions this now means: if there is a lolli:Kid in the story world, and there is an lolli:IceCream, then this Kid takes on the HaveIce goal.

We also specify when the goal has succeeded, by means of the success_condition:

condition(true, [
    fact(Agens, lolli:has, Patiens)
])

where the variables Agens and Patiens are bound (by a precondition match) to a specific lolli:Kid individual and a specific lolli:IceCream individual.

Plot Threads

Now that we have a domain specific ontology, a story world setting, a goal, and actions that characters could be performing, it is time to define plot threads. Plot threads are plot-level actions that initiate new scenes. The simplest story consists of one scene. A plot thread is a Prolog predicate:

thread_schema(...). For the lollipop domain:

thread_schema([
    type('http://www.owl-ontologies.com/StoryWorldSettings/Lollipop.owl#BuyingIce'),
    preconditions([
        condition(true, [
            rule(Kid, owlr:typeOrSubType, lolli:'Kid'),
            rule(Seller, owlr:typeOrSubType, lolli:'IceVendor'),
            fact(Kid, lolli:at, Location)
        ])
    ]),
    characters([
        character(Kid),
        character(Seller)
    ]),
    location(Location),
    settings([]),
    goals([
        goal(Kid, S)
    ])
]) :-
   goal_schema(S),
   schema_type(S, lolli:'HaveIce'),
   schema_agens(S, Kid),
   validate_schema(S, []).

The preconditions make sure that the variables Kid and Seller are bound to Individuals of the correct role types.

The Virtual Storyteller uses the schema to read several types of information:

Above thread_schema definition is dependent on a list of prolog statements that "create" a goal for the Kid in the schema. We give this Kid (presumably Linda) the goal to have ice as an episodic goal, we select this goal by selecting all goal schemas (S) that have Kid as agens (in the arguments), and that are of type HaveIce, and whose preconditions are true (validate_schema). The predicate can be read as follows: "IF there is a goal schema of type lolli:HaveIce, and has Kid as agens, and that goal schema is valid in the current world state (validate_schema), THEN there is a thread schema of type lolli:BuyingIce available.

We place this thread schema in a file buying_ice.pl, and include this by lollipop/threads/threads.pl. That's it! At the beginning of the simulation, the Plot Agent looks which scenes are possible, and asserts setting facts based on that, starts up new Character Agents where possible, asks these to play the given roles, and gives them their (episodic) goals.

The final thing we have to do, is "enable" this domain. We can do this by adding an entry to the vst.properties file in the lib directory of the Virtual Storyteller software:

story_domain.lollipop.name=lollipop
story_domain.lollipop.desc=Linda and Otto engage in buying an ice cream; tutorial domain for starting VST authors.
story_domain.lollipop.author=Ivo
story_domain.lollipop.ontology=http://www.owl-ontologies.com/StoryWorldSettings/Lollipop.owl
story_domain.lollipop.ontology.local=knowledge/domain/lollipop_dialogue/ontology/Lollipop.owl
ALERT! Because the ontology isn't really located at http://www.owl-ontologies.com, we add a local address of the ontology.

Now everything is ready to run the Virtual Storyteller in our freshly made story domain.

Running

In principle, you can run a story by starting up a World Agent and a Plot Agent using the Agent Launcher (vs.AgentLauncher). First, JADE has to be started (with the Start JADE button). In the Plot Agent, you step through the rounds of the story by clicking the "Next Round" button. As just suggested, the Plot Agent will automatically launch the necessary characters, based on the thread schema definition(s). But if you know the number of characters of your story world (in our case, 2), you can also manually start them.

And voila, after a few rounds the story is finished, and you can choose (in the Plot Agent) to save its fabula, which is a TriG file.

ALERT! Otto never knows what to do (and the character agent that plays otto will say so). This is ofcourse because we haven't given him any goal to pursue.

ALERT! if you choose to have characters start up automatically, there will be three Character Agents launched rather than the expected two (linda and otto); this is because of a naive (but faster) implementation of the Character Manager (CM), the module that handles the launching and casting of characters, that will start up an agent for each character it needs when there is none free to cast. After an agent has been cast (e.g. the first launched agent becomes Linda), the CM again looks whether it still needs new characters (yes: Otto), and so launches another Character Agent. This is not a problem though (the third character is a reserve and will never be cast -- unless a third character is needed later in the story).

The generated story

A sketch of the causal chains of the generated story can also be made visible in the Plot Agent at any time, by choosing the menu option "show fabula graph". You will see that Linda's episodic goal to get ice led to the execution of two actions: skip to the ice cream truck and buy ice from Otto with the money that she has. Both have perceived this happening (one perception for each effect of the actions) and formed a belief about it (one belief voor each perception).

Debugging

A very important part of making a story domain is debugging. It is very prevalent that errors are being made in the specification of preconditions, effects, scene definitions, etc. that it is necessary also to know how you can check whether everything works as intended and if not, why not.

Mistakes often made

If something doesn't work as you thought it would, it is possible that you made one of the following mistakes that are easy to make:

Useful debugging predicates in Prolog

By running the Prolog files "offline", you can manually / directly call their predicates. For the character agent (including the partial order planner), load the file load_character.pl, for the plot agent (including the thread manager) load the file load_plot.pl, and for the world agent (operator execution only) the file load_world.pl. Now you can run predicates in the SWI-Prolog console.

Unit tests
test/0 runs some unit tests for the Prolog code. You can write these yourself (see SWI-Prolog Unit Tests) in the form of .plt files with the same name (preceding the dot) as the modules they test. Example: test.

Making plans
plan/4 creates a plan. Input: the character for whom the plan should be made, the "goal state" of the plan, and a variable that will be bound to the found plan. Example: =plan(lolli:linda, [condition(true, [fact(lolli:linda, lolli:has, lolli:vanilla_ice_1)])], Plan).

To get insight in the constructed plans, you can adapt the file pop.pl, which contains a number of debug/2 predicates at the top of the file, that you can enable (switch from false to true) to show several kinds of planning information during plan construction. You can pause the planning process using CTRL-C, and continue by pressing C again. This way you can inspect intermediate plans.

Checking whether thread definitions work
possible_thread/1 shows which threads are possible (their preconditions are true). In principle this would always have to be at least one, because otherwise a story will never start. Example: possible_thread(T)