LPA Home Page LPA Home Page
Products Support Download About LPA Hot News Search

ProWeb: Expert System

Expert systems are one of the classical applications of Prolog technology, and ProWeb provides the ability to present such systems over the Web. This example consists of a simple expert system shell, which features:

  • "if...then" production rules
  • a question and answer management system
  • both a forward chaining and backward chaining inference engine
This example tries to guess the identity of an animal by asking a series of questions. It has been designed so that as much code as possible is shared between the two inference engines and also to allow an easy conversion to just Prolog (i.e. without ProWeb).

The Underlying Prolog Program

For our knowlege base we are going to use a near English-like syntax; for this we need to declare three new Prolog operators:

:- op( 1190,  fx, (if  ) ).
:- op( 1180, xfx, (then) ).
:- op( 1170, xfy, (and ) ).
These operators allows us to write rules such as:

if   'Animal is mammal'
and  'Animal is herbivore'
and  'Status is pest'
then 'Animal is rodent'.
where 'Animal is mammal', 'Animal is herbivore' and 'Status is pest' are conditions which when all true, imply the conclusion, 'Animal is rodent', is also true.

Sometimes, the conclusion of a rule occurs as a condition in another rule, in which case it is an intermediate (derived) fact: i.e. it occurs on both the left and right hand sides of rules.

Sometimes, the conclusion of a rule occurs nowhere else, in which case it is a terminator: i.e. it only occurs on the right hand side of rules.

Sometimes, a condition in a rule occurs nowhere else, in which case it is an askable item: i.e. it only occurs on the left hand side of rules.

The Backward-Chaining Inference Engine

The backward-chaining (also called top-down) inference engine tries to prove a goal by establishing the truth of its conditions; i.e. given the rule 'if A and B then C', the backward-chaining engine will try to prove C by first proving A and then proving B. Proving these conditions (or sub-goals) to be true, may well invoke further calls to the engine, and so on. To get the best results with backward-chaining, we should seed it with a 'top-level' goal to prove. The computation will then either terminate with success (i.e. all the sub-goals can be proved) or with failure (i.e. at least one of the sub-goals is not true).

The backward chaining engine needs a top-level goal to prove. Without one being supplied by the user, all the provable top-level goals are generated using a utility program chain_terminal_conclusion/1.

chain_top_down :-
   chain_terminal_conclusion( R ),
   chain_demo( R ),
   chain_post_result( conclusive(R) ).
If we cannot find a top-level goal which can be proven, then we flag an inconclusive result:

chain_top_down :-
   chain_post_result( inconclusive ).
chain_demo/1 is the main loop of the backward-chaining inference engine. Given the rule 'if A and B then C', then to prove C, we must first prove both A and B:

chain_demo( (A and B) ) :-
   !,
   chain_demo( A ),
   chain_demo( B ).
Given a conclusion to prove, find a rule with this as its conclusion and then try and prove that that rule's conditions are true:

chain_demo( Conclusion ) :-
   ( if Conditions then Conclusion ),
   chain_demo( Conditions ).
If no rule exists for that conclusion, then it must be an askable item:

chain_demo( Q ) :-
   \+ (if _ then Q),
   !,
   chain_user_confirmation( Q ).

The Forward-Chaining Inference Engine

A forward chaining (also called bottom-up) inference engine takes a rule, and if its conditions are true, adds its conclusion to working memory, until no more rules can be applied; i.e. if the conditions of the rule 'if A and B then C' are true, then C is added to working memory.

The first clause of chain_bottom_up/0 checks to see if an end goal has been added to working memory, and if so, a conclusive result has been achieved:

chain_bottom_up :-
   chain_terminal_conclusion( R ),
   chain_working_memory( R ),
   chain_post_result( conclusive(R) ).
The second clause of chain_bottom_up/0 is the main loop for the forward-chaining inference engine. It 'picks-up' the first rule and checks that its conclusion is not already in working memory. If the conclusion has still to be proven, it checks to see whether the rule's conditions are potentially 'satisfiable'. This means checking that none of the rule's conditions have previously been denied (i.e. we have no indications that any of its conditions are false). Then we try to establish the rule either by propagation or by asking questions. Initially, the question Flag is set to 'dont_ask', which supresses the asking of additional questions; upon backtracking, Flag is set to 'ask', which enables questions to be asked.

This means that:

  • we don't ask questions until we've exhausted all the inferencing possible, and
  • we avoid asking questions relating to rules which can never be true
When we find a rule whose conditions are both satisfiable and satisfied, then we add this rule's conclusion to working memory and continue round again. chain_satisfiable/1 ensures that all the conditions of a rule are potentially satisfiable before proceeding onto chain_satisfied/2. i.e. given the rule 'if A and B and C then D', we would not want to ask A and B as questions once we know that C has already been denied:

chain_bottom_up :-
   (  Flag = dont_ask
   ;  Flag = ask
   ),
   ( if C then R ),
   \+ chain_working_memory( R ),
   chain_satisfiable( C ),
   chain_satisfied( C, Flag ),
   !,
   chain_add_working_memory( R ),
   chain_bottom_up.
If we can't infer a 'terminator', and we've exhausted the rules, then we have an inconclusive result:

chain_bottom_up :-
   chain_post_result( inconclusive ).
chain_working_memory/1 checks to see if we've already deduced that the rule is true:

chain_working_memory( R ) :-
   proweb_call( chain_working_memory( R ) ).
The first clause of chain_satisfiable/1 deals with conjunction; rules with two or more conditions:

chain_satisfiable( (A and B) ) :-
   !,
   chain_satisfiable( A ),
   chain_satisfiable( B ).
Intermediate derived conclusions must already be in working memory:

chain_satisfiable( R ) :-
   ( if _ then R ),
   !,
   chain_working_memory( R ).
The final clause of chain_satisfiable/1 takes each condition passed and ensures that it is potentially satisfiable by checking that it has not already been denied. chain_satisfiable/1 will fail as soon as one condition is not potentially satisfiable (i.e. it has already been denied).

chain_satisfiable( Q ) :-
   \+ chain_user_answer( Q, no ).
chain_satisfied/2 succeeds when all related answers are 'yes' and all intermediate conclusions are in working memory.

The first clause of chain_satisfied/2 deals with conjunction; rules with two or more conditions:

chain_satisfied( (A and B), Flag ) :-
   !,
   chain_satisfied( A, Flag ),
   chain_satisfied( B, Flag ).
Intermediate derived conclusions must already be in working memory:

chain_satisfied( R, _Flag ) :-
   ( if _ then R ),
   !,
   chain_working_memory( R ).
Where we have an askable condition whilst in 'ask' mode, then ask the user to confirm the condition:

chain_satisfied( Q, ask ) :-
   chain_user_confirmation( Q ).
chain_add_working_memory/1 then adds the rule's conclusion to working memory:

chain_add_working_memory( Conclusion ) :-
   proweb_assert( chain_working_memory( Conclusion ) ).

Utilities

The code in this section is shared by both the forward and the backward chaining inference engines.

A 'top level' goal is any conclusion which does not occur as a condition in another rule; chain_terminal_conclusion/1 identifies which are the 'top level' goals:

chain_terminal_conclusion( R ) :-
   ( if _ then R ),
   \+ chain_condition( R ).

chain_condition( C ) :-
   ( if A then _ ),
   chain_condition( A, C ).

chain_condition( C, C ).

chain_condition( (A and _B), C ) :-
   chain_condition( A, C ).

chain_condition( (_A and B), C ) :-
   chain_condition( B, C ).
chain_user_confirmation/1 checks to see if a condition is true by first checking for a previous answer, or if that fails, by asking a new question (but not both); in either case, the answer must be 'yes' for the call to chain_user_confirmation/1 to succeed:

chain_user_confirmation( Q ) :-
   (  chain_user_answer( Q, Answer )
   -> true
   ;  chain_user_question( Q, Answer )
   ),
   Answer = yes.
The chain_user_question/2 clause sends an HTML page, asking the required question, to the user, and upon return, processes the answer:

chain_user_question( Q, Answer ) :-
   proweb_send_form( chain_ask_question_form(Q) ),
   chain_user_answer( Q, Answer ).
As soon as a result, either conclusively or inconclusively, can be given, chain_post_result/1 will be called:
chain_post_result( Result ) :-
   proweb_send_form( chain_result(Result) ).

Question and Answer Management System

To make the questions asked by the system more user-friendly, a number of clauses to handle 'canned' text for the various questions are also supplied:

question( 'Blood is cold',
	  `Is it cold blooded?`,
	  ['Blood is warm'-no], 
	  ['Blood is warm'-yes]).
When the condition 'Blood is cold' needs to be asked as a question, the second argument is 'picked-up' (i.e. `Is it cold blooded?`) and used within the HTML page instead.

Furthermore, we can supply the names of other questions (i.e. 'Blood is warm') which are in effect also answered when a particular question is answered. This way, we can avoid asking redundant questions. The third argument of question/4 is a list of facts to be derived if the answer to the question is 'yes' (i.e. ['Blood is warm'-no]), and the fourth argument, a list of facts to be derived if the answer is 'no' (i.e. ['Blood is warm'-yes]).

It is chain_user_answer/2 that returns the answer to a question. It is either directly 'picked up' as the answer to a question or indirectly as an assertion performed following the answering of a previous question:

chain_user_answer( Q, Answer ) :-
   proweb_returned_answer(
			   chain_question(Q),
			   Answer
			 ),
   assert_related_facts( Q, Answer ).

chain_user_answer( Q, Answer ) :-
   proweb_call( fact(Q,Answer) ).
assert_related_facts/2 is called following the answering of a question. The list of facts to be derived following a yes answer is assigned to YesFactList (i.e. the third argument of question/4); likewise, the list of facts to be derived following a no answer is assigned to NoFactList (i.e. the last argument of question/4). member/2 then selects the appropriate FactList depending on the answer to the question, whereupon the facts are asserted.

assert_related_facts( Q, YesNo ) :-
   question( Q, _, YesFactList, NoFactList ),
   member( YesNo-FactList,
	   [yes-YesFactList,no-NoFactList]
	 ),
   forall( member(A-V,FactList),
	   proweb_unique_assert(fact(A,V))
	 ).
proweb_assert/1 will perform an assertion even if the assertion is already present; the user-defined proweb_unique_assert/1 clause below does not:

proweb_unique_assert( X ) :-
   \+ proweb_call( X ),
   proweb_assert( X ).

The ProWeb Front-End

As this example of one of several on the LPA web site, we need to declare the user-defined proweb predicates as being 'multifile':

:- multifile( proweb_page    /2 ).
:- multifile( proweb_form    /2 ).
:- multifile( proweb_question/2 ).
As we only want to ask one question at a time, the following ProWeb directive is also required:

:- proweb_setting( max_forms_per_page, 1 ).
A number of derived facts will need to be asserted into the Prolog internal database and remain unique to each client:

:- proweb_dynamic( fact/2 ).

The Ask Question Form

The chain_ask_question_form is used to ask a question of the client:

proweb_page( [ chain_ask_question_form(_) | _ ],
             [
               include('ess\head.htm'),
               `Chain Ask Question Form`,
               include('ess\body.htm'),
               h1 @ `Guess An Animal`,
               center,
               proweb(form),
               /center,
               include('ess\foot.htm')
             ]
           ).

proweb_form( chain_ask_question_form(Q),
             [ QT,
               p,
               ?chain_question(Q)
             ]
           ) :- question( Q, QT, _, _ ).

proweb_question( chain_question(_),
                 [ method  = radio,
                   type    = atom,
                   select  = [yes,no],
                   prefill = yes
                 ] ).

The Result Form

The purpose of the result form is to let the client know what has been concluded (or not as the case may be):

proweb_page( [ chain_result(_) | _ ],
             [ 
               include('ess\head.htm'),
               `Chain Result Form`,
               include('ess\body.htm'),
               h1 @ `Guess An Animal`,
               center,
               proweb(form),
               /center,
               include('ess\foot.htm')
             ]
           ).

proweb_form( chain_result( conclusive(R) ),
             [ `I have concluded that: `,
               b @ verbatim @ R
             ]
           ).

proweb_form( chain_result( inconclusive ),
             `I cannot conclude anything (else) !`
           ).

The Animal Taxonomy Knowledge Base

The animal taxonomy knowledge base is as follows:

if   'Blood is warm'
then 'Animal is warm blooded'.

if   'Blood is cold'
then 'Animal is cold blooded'.

if   'Animal is warm blooded'
and  'Skin is fur'
and  'Motion is walk'
and  'Motion is swim'
then 'Animal is mammal'.

if   'Animal is warm blooded'
and  'Skin is feather'
and  'Habitat is tree'
and  'Motions are fly'
then 'Animal is sky based bird'.

if   'Animal is warm blooded'
and  'Skin is feather'
and  'Habitat is land'
and  'Motion is walk'
and  'Motion is swim'
and  'Size is medium'
then 'Animal is land based bird'.

if   'Animal is cold blooded'
and  'Skin is scale'
and  'Habitat is the sea'
and  'Motions are swim'
then 'Animal is sea water fish'.

if   'Animal is cold blooded'
and  'Skin is scale'
and  'Habitat is a river'
and  'Motions are swim'
then 'Animal is fresh water fish'.

if   'Meal is meat'
then 'Animal is carnivore'.

if   'Meal is plant'
then 'Animal is herbivore'.

if   'Animal is land based bird'
and  'Colour is blank and white'
then 'Animal is penguin'.

if   'Animal is sea water fish'
and  'Colour is pink'
then 'Animal is salmon'.

if   'Animal is fresh water fish'
and  'Colour is pink'
then 'Animal is salmon'.

if   'Animal is warm blooded'
and  'Speed is very fast'
and  'Legs are four'
then 'Animal is feline'.

if   'Animal is feline'
and  'Animal is carnivore'
and  'Size is medium'
then 'Animal is medium cat'.

if   'Animal is feline'
and  'Meal is human'
and  'State is predator'
and  'Size is large'
then 'Animal is big cat'.

if   'Animal is big cat'
and  'Habitat is jungle'
and  'Tail is long and furry'
then 'Animal is tiger'.

if   'Animal is medium cat'
and  'Tail is long and furry'
then 'Animal is cat'.

if   'Animal is medium cat'
and  'Tail is missing'
then 'Animal is manx cat'.

if   'Animal is mammal'
and  'Animal is herbivore'
and  'Status is pest'
then 'Animal is rodent'.

if   'Animal is rodent'
and  'Tail is short and thin'
and  'Habitat is sewer'
then 'Animal is rat'.

if   'Animal is rodent'
and  'Size is small'
and  'Tail is long and bushy'
and  'Colour is grey'
then 'Animal is grey squirrel'.

The question/4 Clauses

In addition to the knowledge base, a number of clauses to handle 'canned' text for the various questions are also supplied:

question( 'Blood is cold',
	  `Is it cold blooded?`,
	  ['Blood is warm'-no], 
	  ['Blood is warm'-yes]).

question( 'Blood is warm', 
	  `Is it warm blooded?`,
	  ['Blood is cold'-no],
	  ['Blood is cold'-yes]).

question( 'Colour is black and white', 
	  `Is it black and white in colour?`, 
	  ['Colour is grey'-no,'Colour is pink'-no], 
	  []).

question( 'Colour is grey', 
	  `Is it grey in colour?`, 
	  ['Colour is black and white'-no,
	   'Colour is pink'-no], 
	  []).

question( 'Colour is pink', 
	  `Is it pink in colour?`, 
	  ['Colour is black and white'-no,
	   'Colour is grey'-no], 
	  []).

question( 'Habitat is sewer',
	  `Does it live in the sewer?`,
	  [],
	  []).

question( 'Habitat is jungle',
	  `Does it live in the jungle?`,
	  [],
	  []).

question( 'Habitat is tree',
	  `Does it live in a tree?`,
	  ['Habitat is the sea'-no,
	   'Habitat is a river'-no],
	  []).

question( 'Habitat is land',
	  `Does it live on the land?`,
	  ['Habitat is the sea'-no,
	   'Habitat is a river'-no],
	  []).

question( 'Habitat is the sea',
	  `Does it live in the sea?`,
	  ['Habitat is land'-no],
	  []).

question( 'Habitat is a river',
	  `Does it live in a river?`,
	  ['Habitat is land'-no],
	  []).

question( 'Meal is meat',
	  `Does it eat meat?`,
	  ['Meal is plant'-no],
	  ['Meal is plant'-yes]).

question( 'Meal is plant',
	  `Does it eat plants?`,
	  ['Meal is meat'-no],
	  ['Meal is meat'-yes]).

question( 'Motion is walk',
	  `Does it walk?`,
	  [],
	  []).

question( 'Motion is swim',
	  `Does it swim?`,
	  [],
	  []).

question( 'Motion is fly',
	  `Does it fly?`,
	  [],
	  []).

question( 'Size is small',
	  `Is it small in size?`,
	  ['Size is medium'-no,'Size is large'-no],
	  []).

question( 'Size is medium',
	  `Is it medium in size?`,
	  ['Size is small'-no,'Size is large'-no],
	  []).

question( 'Size is large',
	  `Is it large in size?`,
	  ['Size is small'-no,'Size is medium'-no],
	  []).

question( 'Skin is fur',
	  `Is it covered in fur?`,
	  ['Skin is feather'-no,'Skin is scale'-no],
	  []).

question( 'Skin is feather',
	  `Is it covered in feathers?`,
	  ['Skin is fur'-no,'Skin is scale'-no],
	  []).

question( 'Skin is scale',
	  `Is it covered in scales?`,
	  ['Skin is fur'-no,'Skin is feather'-no],
	  []).

question( 'Status is pest',
	  `Is it considered a pest?`,
	  [],
	  []).

question( 'State is predator',
	  `Is it considered a predator?`,
	  [],
	  []).

question( 'Tail is long and bushy',
	  `Is its tail long and bushy?`,
	  ['Tail is missing'-no,
	   'Tail is long and furry'-no,
	   'Tail is short and thin'-no],
	  []).

question( 'Tail is short and thin',
	  `Is its tail short and thin?`,
	  ['Tail is missing'-no,
	   'Tail is long and furry'-no,
	   'Tail is long and bushy'-no],
	  []).

question( 'Tail is long and furry',
	  `Is its tail long and furry?`,
	  ['Tail is missing'-no,
	   'Tail is long and bushy'-no,
	   'Tail is long and bushy'-no],
	  []).

question( 'Tail is missing',
	  `Is its tail missing?`,
	  ['Tail is long and bushy'-no,
	   'Tail is long and furry'-no,
	   'Tail is short and thin'-no],
	  []).
If there is no question/4 entry for a particular question, then the following generic routine is called to ask it:

question( Q, QT, [], [] ) :-
   (  write(`Is it true that `),
      write(Q),
      write(`?`)
   ) ~> QT.