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.
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).
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.
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:
at
(to express location of objects, e.g., linda at park
)
has
(to express ownership, e.g., linda has money
)
adjacent
(to spatially connect locations, e.g., park adjacent icecreamtruck
)
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.
p1
in fabula
)
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.
at
) and Otto (at his ice cream truck, at
)
adjacent
)
has
)
has
)
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
.
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.
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. 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.
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.
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.
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.
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:
characters([...])
)
goals([...])
)
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.owlBecause 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.
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.
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.
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).
fact(...)
when the dots do not describe a fact, but a rule predicate, for instance fact(Agens, owlr:typeOrSubType, lolli:'Kid')
is wrong.fact(Agens, lolli:has, lolli:'IceCream')
is wrong.rule(Patiens, owlr:typeOrSubType, lolli:IceCream)
is wrong. Prolog thinks IceCream
is a variable.
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.
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.
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.
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)