In the previous part of this series, we built a DynamicForm class to generate dialogue boxes for user input. Now, we’re taking it a step further, implementing the knowledge base and designing a way for an expert to store the queries for the user to answer.
For the following code, we will be building upon the ExpertSystemPart1.html file that we’ve created previously. Click here to see the previous post. To preview the finished demo, click here.
Getting Started
What we want to achieve in this part is to have an expert begin creating a knowledge base using queries and prompts from the user. To do that we will be building three classes, Query, KnowledgeBase, and Expert. We start by defining the role and responsibilities of each.
What is a Query?
A Query is simply an object that stores a question to be asked and a way to store the answer in sets of yeses and noes. We will do that by using the JavaScript Set object which allows us to store unique values of strings.
Copy the code below and paste it under the DynamicForm class that we’ve created.
//##############################################################################// Query.js//##############################################################################(function() { "use strict"; function Query(question, context, yeses, nos) { this.question = question; this.context = context; this.yeses = yeses || new Set(); this.nos = nos || new Set(); return this; }; var p = Query.prototype; p.export = function() { return { question: this.question, context: this.context, yeses: Array.from(this.yeses), nos: Array.from(this.nos) }; }; p.import = function(obj) { this.question = obj.question; this.context = obj.context; this.yeses = new Set(obj.yeses); this.nos = new Set(obj.nos); return this; }; trf.Query = Query;}());
To understand this better, we will use our animal expert as an example. If our list of possible answers for the animal expert is horse, duck, and chicken. A simple question could be “Does the animal have hooves?”. In which case the horse would go to the yeses set, and the duck and chicken would go to the noes set. This will be important later when the list becomes bigger and we use the queries to deduce the next best question to ask.
Along with the question, we also store the optional context for the question. This is just something optional that can help the user figure out how to answer our questions.
Lastly. we also want to be able to import and export a query from and to a JSON file so we can access the Query later. For the export we simply turn our sets into arrays, and for the import we assign the values of our query based on the parsed JSON object that’s passed in.
p.export = function() { return { question: this.question, context: this.context, yeses: Array.from(this.yeses), nos: Array.from(this.nos) };};p.import = function(obj) { this.question = obj.question; this.context = obj.context; this.yeses = new Set(obj.yeses); this.nos = new Set(obj.nos); return this;};
What is a Knowledge Base?
A Knowledge Base is an object that stores all the Queries and all the possible answers for a particular subject. It needs to also store useful information like the key for accessing the information from localStorage, and the initial instruction for the user to begin.
Copy the Knowledge Base code and paste below the Query class.
//##############################################################################// KnowledgeBase.js//##############################################################################(function() { "use strict"; function KnowledgeBase() { this.key = null; }; var p = KnowledgeBase.prototype; p.create = function(subject, instruction){ this.answers = new Set(); this.answerContexts = new Map(); this.queries = new Set(); this.subject = subject; this.instruction = instruction; this.key = 'trf-' + subject; } p._toJSONString = function(space) { let queries = []; for (const query of this.queries) { queries.push(query.export()); } const obj = { subject: this.subject, instruction: this.instruction, answers: [...this.answerContexts], queries: queries }; return JSON.stringify(obj, null, space); }; p._loadFromJSONString = function(JSONString) { if (!JSONString) { return false; }; const obj = JSON.parse(JSONString); this._loadFromJSONObject(obj); }; p._loadFromJSONObject = function(JSONObject) { const obj = JSONObject; this.answers = new Set(obj.answers.map(row => row[0])); this.queries = new Set(); for (const query of obj.queries) { this.queries.add(new trf.Query().import(query)); }; this.answerContexts = new Map(obj.answers); this.subject = obj.subject; this.instruction = obj.instruction; this.key = 'trf-' + obj.subject; }; p.saveToLocalStorage = function() { localStorage.setItem(this.key, this._toJSONString()); }; p.loadFromLocalStorage = function(key) { this._loadFromJSONString(localStorage.getItem(key)); }; p.deleteAnswer = function(answer){ this.answers.delete(answer); this.answerContexts.delete(answer); for (const query of this.queries) { query.yeses.delete(answer); query.nos.delete(answer); } }; trf.KnowledgeBase = KnowledgeBase;}());
Let’s break the knowledge base down to get a better understanding of how this class works. First, in the constructor we set the key to null, indicating that the knowledge base is empty. The knowledge base has to either be newly created or loaded from a saved source. In this case, we will be using localStorage in the browser to save our data.
function KnowledgeBase() { this.key = null;};
The create function is used when no existing knowledge base exists. You simply pass in the subject and instruction strings, and it will create a new knowledge base for you. For better searching and indexing, we add trf- as a prefix to the key.
The answerContexts is an optional parameter that helps explain the answer. In the animal expert example, this would be a fun fact about the animal. We use the JavaScript Map object to make it easier to index.
p.create = function(subject, instruction){ this.answers = new Set(); this.answerContexts = new Map(); this.queries = new Set(); this.subject = subject; this.instruction = instruction; this.key = 'trf-' + subject; }
The function _toJSONString deconstructs the knowledge base into a readable JSON string by turning everything into arrays. We used the underscore to indicate this is a private function, and it will return a JSON string.
p._toJSONString = function(space) { let queries = []; for (const query of this.queries) { queries.push(query.export()); } const obj = { subject: this.subject, instruction: this.instruction, answers: [...this.answerContexts], queries: queries }; return JSON.stringify(obj, null, space);};
In the reverse, we have the function _loadFromJSONString, which takes the JSON string parse it as an object which the knowledge base will use to construct itself.
p._loadFromJSONString = function(JSONString) { if (!JSONString) { return false; }; const obj = JSON.parse(JSONString); this._loadFromJSONObject(obj);};p._loadFromJSONObject = function(JSONObject) { const obj = JSONObject; this.answers = new Set(obj.answers.map(row => row[0])); this.queries = new Set(); for (const query of obj.queries) { this.queries.add(new trf.Query().import(query)); }; this.answerContexts = new Map(obj.answers); this.subject = obj.subject; this.instruction = obj.instruction; this.key = 'trf-' + obj.subject;};
Finaly, we have public functions to save and load the database, as well as a helper function to delete an answer entry from the knowledge base if that becomes necessary.
p.saveToLocalStorage = function() { localStorage.setItem(this.key, this._toJSONString());};p.loadFromLocalStorage = function(key) { this._loadFromJSONString(localStorage.getItem(key));};p.deleteAnswer = function(answer){ this.answers.delete(answer); this.answerContexts.delete(answer); for (const query of this.queries) { query.yeses.delete(answer); query.nos.delete(answer); }};
What is an Expert?
An expert puts everything we’ve built together. It uses DynamicForm to ask the user questions and builds the KnowledgeBase using Query. The idea here is for the expert to impart enough knowledge into the program so that users can use the program even after the expert is gone.
We’ll start with a basic template for the Expert Class. Copy and paste the code below the KnowledgeBase Class.
//##############################################################################// Expert.js//##############################################################################(function() { "use strict"; function Expert(elementId) { this.dynamicForm = new trf.DynamicForm(elementId); this.knowledgeBase = new trf.KnowledgeBase(); }; var p = Expert.prototype; // Add functions here trf.Expert = Expert;}());
First, we need to ask if the user wants to load the existing knowledge base or create a new one. After that we will greet the user with the subject matter and the instructions.
To get a taste of how this all works, copy and paste the functions below and add it to the Expert Class.
p.askForKnowledgeBase = function() { this.dynamicForm.create({ title: 'Which knowledge base should I use?', fields: [ { type: 'label', text:'Select where to obtain the knowledge base'}, { type: 'buttons', options: [ {type: 'yes', text: 'New', callback: this.createNewKnowledgeBase.bind(this)}, {type: 'yes', text: 'Browser', callback: this.selectBrowserKnowledgeBase.bind(this)}, ] } ] });};p.createNewKnowledgeBase = function() { this.dynamicForm.create({ title: 'Create a new Knowledgebase', fields: [ { type: 'label', text:'Subject:'}, { type: 'textbox', placeholder: 'Example: Animal' , name: 'subject'}, { type: 'label', text:'Instruction:'}, { type: 'textarea', placeholder: 'Example: Please think of an animal' , name: 'instruction'}, { type: 'buttons', options: [ {type: 'yes', text: 'Ok', callback: this._createNewKnowledgeBase.bind(this)}, {type: 'no', text: 'Cancel', callback: this.askForKnowledgeBase.bind(this)}]} ] });};p._createNewKnowledgeBase = function() { const subject = this.dynamicForm.textboxes.get('subject').value; const instruction = this.dynamicForm.textboxes.get('instruction').value; if (!subject) { alert("Need to name a subject"); return; } this.knowledgeBase.create(subject, instruction); this.greet();};p.selectBrowserKnowledgeBase = function(){ const keys = []; for (const key of Object.keys(localStorage)){ if (key.slice(0, 4) === 'trf-') { keys.push(key); }; }; if (keys.length === 0) { alert("No knowledge bases found in browser storage"); this.createNewKnowledgeBase(); return; } this.dynamicForm.create({ title: 'Select a Knowledgebase', fields: [ { type: 'select', options: keys }, { type: 'buttons', options: [ {type: 'yes', text: 'Ok', callback: this._selectBrowserKnowledgeBase.bind(this)}, {type: 'no', text: 'Cancel', callback: this.askForKnowledgeBase.bind(this)} ]} ] });};p._selectBrowserKnowledgeBase = function(){ const key = this.dynamicForm.selects.get('select').value this.knowledgeBase.loadFromLocalStorage(key); this.greet();};p.greet = function() { this.possibleAnswers = new Set(this.knowledgeBase.answers); this.notAskedQueries = new Set(this.knowledgeBase.queries); this.deferQueries = new Set(); this.askedQueries = new Map(); this.dynamicForm.create({ title: 'Hello, I am your ' + this.knowledgeBase.subject + ' expert', fields: [ { type: 'label', text:this.knowledgeBase.instruction }, { type: 'buttons', options: [ {type: 'yes', text: 'Ok', callback: this._guess.bind(this)}, {type: 'no', text: 'Quit', callback: this.quit.bind(this)}] } ] });};p._guess = function() { // insert code here};p.quit = function() { this.dynamicForm.create({ title: 'Save knowledge base?', fields: [ { type: 'buttons', options: [ {type: 'yes', text: 'Yes', callback: this._quit.bind(this)}, {type: 'no', text: 'Cancel', callback: this.greet.bind(this)}, {type: 'no', text: 'Quit', callback: this.askForKnowledgeBase.bind(this)} ] } ] });};p._quit = function() { this.knowledgeBase.saveToLocalStorage(); alert('Knowledge base saved to browser'); this.askForKnowledgeBase();};
A couple of things to note for the code above. First, whenever we used a callback for the buttons, the bind function is used to maintain the correct scope for this.
this.createNewKnowledgeBase.bind(this)
When selecting the knowledge base for the expert, we used trf- prefix to find the appropriate keys. If none are found, then the revert to creating a new knowledge base.
p.selectBrowserKnowledgeBase = function(){ const keys = []; for (const key of Object.keys(localStorage)){ if (key.slice(0, 4) === 'trf-') { keys.push(key); }; }; if (keys.length === 0) { alert("No knowledge bases found in browser storage"); this.createNewKnowledgeBase(); return; }; this.dynamicForm.create({ title: 'Select a Knowledgebase', fields: [ { type: 'select', options: keys }, { type: 'buttons', options: [ {type: 'yes', text: 'Ok', callback: this._selectBrowserKnowledgeBase.bind(this)}, {type: 'no', text: 'Cancel', callback: this.askForKnowledgeBase.bind(this)} ]} ] });};
When we get to the greet function, we created new sets and maps for answers and queries. These will be used later to keep track of which animals were guessed, and which questions were asked.
p.greet = function() { this.possibleAnswers = new Set(this.knowledgeBase.answers); this.notAskedQueries = new Set(this.knowledgeBase.queries); this.deferQueries = new Set(); this.askedQueries = new Map(); this.dynamicForm.create({ title: 'Hello, I am your ' + this.knowledgeBase.subject + ' expert', fields: [ { type: 'label', text:this.knowledgeBase.instruction }, { type: 'buttons', options: [ {type: 'yes', text: 'Ok', callback: this._guess.bind(this)}, {type: 'no', text: 'Quit', callback: this.quit.bind(this)}] } ] });};
Finally, to test everything out, remove the code in the main program and replace it with this.
//##############################################################################// Main Program//##############################################################################var expert = new trf.Expert("formContainer");expert.askForKnowledgeBase();
Now you can run the html file in your browser and see the dialogue boxes.

If you ever want to edit or delete the knowledge base created, simply right click on the browser and click Inspect. Then in the panel action bar, click on Application and the keys are stored in file:// under local storage.

Next Steps
Another part done. We now have all the pieces to put together the AI. All we need now is to guess function to complete the inference engine. Click here to see the file for this part of the project. See you in part 3!









