From a5efe6738274c86260a560ebdae704b548d9729a Mon Sep 17 00:00:00 2001 From: Timotheus Kampik Date: Thu, 27 Jun 2024 09:44:59 +0200 Subject: [PATCH] Support functional beliefs And a corresponding belief revision function --- README.md | 66 +++++++++++++++- spec/src/agent/Agent.spec.js | 65 ++++++++++++++++ spec/src/agent/Belief.spec.js | 2 +- spec/src/agent/FunctionalBelief.spec.js | 46 +++++++++++ .../beliefRevision/revisionFunctions.spec.js | 25 +++++- src/agent/Agent.js | 20 ++++- src/agent/FunctionalBelief.js | 21 +++++ src/agent/beliefRevision/revisionFunctions.js | 76 ++++++++++++++++++- 8 files changed, 313 insertions(+), 8 deletions(-) create mode 100644 spec/src/agent/FunctionalBelief.spec.js create mode 100644 src/agent/FunctionalBelief.js diff --git a/README.md b/README.md index d42c439..9d66e52 100644 --- a/README.md +++ b/README.md @@ -618,7 +618,7 @@ const update = { const agent = new Agent('myAgent', beliefBase, {}, [], undefined, false, revisePriority) ``` -After applying the belief update, our agent's belief base is as follows: +After applying the belief update with `agent.next(update)`, our agent's belief base is as follows: ```JavaScript { @@ -647,6 +647,70 @@ const beliefBase = { } ``` +We may want to specify beliefs that are not simply updated as static objects/properties, but dynamically inferred, based on our current belief base or updates thereof. +To supports this, JS-son uses the notion of a *functional belief*. +A functional belief can be specified as follows, for example: + +```JavaScript +const isSlippery = FunctionalBelief( + 'isSlippery', + false, + (oldBeliefs, newBeliefs) => + (newBeliefs.isRaining && newBeliefs.isRaining.value) || + (!newBeliefs.isRaining && oldBeliefs.isRaining && oldBeliefs.isRaining.value), + 1 + ) +``` + +The arguments of `FunctionalBelief` have the following meaning: + +* `isSlippery` (`id`) is the unique identifier of the (functional) belief, +* `false` (`value`) is the belief's default/initial value; +* The function: + ```JavaScript + (oldBeliefs, newBeliefs) => + (newBeliefs.isRaining && newBeliefs.isRaining.value) || + (!newBeliefs.isRaining && oldBeliefs.isRaining && oldBeliefs.isRaining.value) + ``` + specifies the rule according to which the belief is inferred -- in this simple example, the value of `isSlippery` takes the value of the new belief `isRaining` unless the belief does not exist, in which it will check for the existing (old) belief `isRaining` and return `false` if neither a new nor an old `isRaining` belief exists. +* `0` (`order`) is the number used for inducing the order in which the functional belief is revised relative to other functional beliefs: e.g., if another functional belief with order `1` is present, then the latter belief is revised later. +* `2` (`priority`) is the priority that the belief takes when updating the function as well as the default value, analogous to how orders work for non-functional beliefs. + +Given this functional belief, we can now demonstrate how functional belief revision works: + +1. First, we specify our agent: + +```JavaScript + const newAgent = new Agent({ + id: 'myAgent', + beliefs: { isRaining: Belief('isRaining', true, 0) }, + desires, + plans + }) + newAgent.next(newBeliefs1) + expect(newAgent.beliefs.isSlippery.value).toBe(true) + newAgent.next(newBeliefs2) + expect(newAgent.beliefs.isSlippery.value).toBe(false) +``` + +2. Then, we specify the initial belief base and execute the agent's reasoning loop with a belief base update that merely contains the functional belief: + +```JavaScript + newAgent.next({ isSlippery}) +``` +Because ``isRaining`` is not present in the update, our agent infers ``isSlippery`` from its old belief base, i.e., the value of ``isSlippery`` remains ``true``. + +3. Finally, we executed the reasoning loop again, with a slightly different belief base update: + +```JavaScript + newAgent.next({ + isSlippery, + isRaining: Belief('isRaining', false, 0) + }) +``` + +Now, the value of ``isSlippery`` is updated to ``false``, as inferred from the update of ``isRaining``. + ## Messaging JS-son agents can send "private" messages to any other JS-son agent, which the environment will then relay to this agent only. Agents can send these messages in the same way they register the execution of an action as the result of a plan. diff --git a/spec/src/agent/Agent.spec.js b/spec/src/agent/Agent.spec.js index aa07b2d..d3117b2 100644 --- a/spec/src/agent/Agent.spec.js +++ b/spec/src/agent/Agent.spec.js @@ -1,4 +1,5 @@ const Belief = require('../../../src/agent/Belief') +const FunctionalBelief = require('../../../src/agent/FunctionalBelief') const Plan = require('../../../src/agent/Plan') const Agent = require('../../../src/agent/Agent') @@ -235,4 +236,68 @@ describe('Agent / next(), configuration object-based', () => { }) expect(newAgent.next({ ...Belief('dogNice', false) })[0].action).toEqual('Good dog, Hasso!') }) + + it('should correctly revise functional beliefs, given new beliefs', () => { + const oldBeliefs = { + isRaining: Belief('isRaining', true, 0), + } + + const isSlippery = FunctionalBelief('isSlippery', false, (_, newBeliefs) => newBeliefs.isRaining.value, 1) + + const newBeliefs1 = { + isRaining: Belief('isRaining', false, 0), + isSlippery + } + + const newBeliefs2 = { + isRaining: Belief('isRaining', true, 0), + isSlippery + } + + const newAgent = new Agent({ + id: 'myAgent', + beliefs: oldBeliefs, + desires, + plans + }) + newAgent.next(newBeliefs1) + expect(newAgent.beliefs.isSlippery.value).toBe(false) + newAgent.next(newBeliefs2) + expect(newAgent.beliefs.isSlippery.value).toBe(true) + }) + + it('should correctly revise functional beliefs, given new and old beliefs', () => { + const oldBeliefs = { + isRaining: Belief('isRaining', true, 0), + } + + const isSlippery = FunctionalBelief( + 'isSlippery', + false, + (oldBeliefs, newBeliefs) => + (newBeliefs.isRaining && newBeliefs.isRaining.value) || + (!newBeliefs.isRaining && oldBeliefs.isRaining && oldBeliefs.isRaining.value), + 1 + ) + + const newBeliefs1 = { + isSlippery + } + + const newBeliefs2 = { + isSlippery, + isRaining: Belief('isRaining', false, 0) + } + + const newAgent = new Agent({ + id: 'myAgent', + beliefs: oldBeliefs, + desires, + plans + }) + newAgent.next(newBeliefs1) + expect(newAgent.beliefs.isSlippery.value).toBe(true) + newAgent.next(newBeliefs2) + expect(newAgent.beliefs.isSlippery.value).toBe(false) + }) }) diff --git a/spec/src/agent/Belief.spec.js b/spec/src/agent/Belief.spec.js index 56e5198..8ed58ee 100644 --- a/spec/src/agent/Belief.spec.js +++ b/spec/src/agent/Belief.spec.js @@ -2,7 +2,7 @@ const Belief = require('../../../src/agent/Belief') const warning = 'JS-son: Created belief with non-JSON object, non-JSON data type value' -describe('belief()', () => { +describe('Belief()', () => { console.warn = jasmine.createSpy('warn') it('should create a new belief with the specified key and value', () => { diff --git a/spec/src/agent/FunctionalBelief.spec.js b/spec/src/agent/FunctionalBelief.spec.js new file mode 100644 index 0000000..7e56d58 --- /dev/null +++ b/spec/src/agent/FunctionalBelief.spec.js @@ -0,0 +1,46 @@ +const FunctionalBelief = require('../../../src/agent/FunctionalBelief') + +const beliefWarning = 'JS-son: Created belief with non-JSON object, non-JSON data type value' +const functionalBeliefWarning = 'JS-son: functional belief without proper two-argument function' + +describe('FunctionalBelief()', () => { + console.warn = jasmine.createSpy('warn') + + it('should create a new functional belief with the specified key, value, rule, and order', () => { + const functionBelief = FunctionalBelief('isSlippery', false, (_, newBeliefs) => newBeliefs.isRaining.value, 0) + expect(functionBelief.isSlippery).toEqual(false) + expect(functionBelief.value).toEqual(false) + expect(functionBelief.rule.toString()).toEqual(((_, newBeliefs) => newBeliefs.isRaining.value).toString()) + expect(functionBelief.order).toEqual(0) + }) + + it('should create a new functional belief with the specified key, value, rule, order, priority, and priority update spec', () => { + const functionBelief = FunctionalBelief('isSlippery', false, (_, newBeliefs) => newBeliefs.isRaining.value, 0, 2) + expect(functionBelief.isSlippery).toEqual(false) + expect(functionBelief.value).toEqual(false) + expect(functionBelief.rule.toString()).toEqual(((_, newBeliefs) => newBeliefs.isRaining.value).toString()) + expect(functionBelief.order).toEqual(0) + expect(functionBelief.priority).toEqual(2) + }) + + it('should throw warning if base belief is not JSON.stringify-able & not of a JSON data type', () => { + console.warn.calls.reset() + // eslint-disable-next-line no-unused-vars + const functionBelief = FunctionalBelief('function', () => {}, (_, newBeliefs) => newBeliefs.isRaining, 0, 2) + expect(console.warn).toHaveBeenCalledWith(beliefWarning) + }) + + it('should throw warning if rule is not a function', () => { + console.warn.calls.reset() + // eslint-disable-next-line no-unused-vars + const functionBelief = FunctionalBelief('isSlippery', false, true, 0, 2) + expect(console.warn).toHaveBeenCalledWith(functionalBeliefWarning) + }) + + it('should throw warning if rule is a function that does not take exactly two arguments', () => { + console.warn.calls.reset() + // eslint-disable-next-line no-unused-vars + const functionBelief = FunctionalBelief('isSlippery', false, newBeliefs => newBeliefs.isRaining, 0) + expect(console.warn).toHaveBeenCalledWith(functionalBeliefWarning) + }) +}) diff --git a/spec/src/agent/beliefRevision/revisionFunctions.spec.js b/spec/src/agent/beliefRevision/revisionFunctions.spec.js index e453a09..786b73d 100644 --- a/spec/src/agent/beliefRevision/revisionFunctions.spec.js +++ b/spec/src/agent/beliefRevision/revisionFunctions.spec.js @@ -1,10 +1,13 @@ const Agent = require('../../../../src/agent/Agent') const Belief = require('../../../../src/agent/Belief') +const FunctionalBelief = require('../../../../src/agent/FunctionalBelief') const { reviseSimpleNonmonotonic, reviseMonotonic, revisePriority, - revisePriorityStatic } = require('../../../../src/agent/beliefRevision/revisionFunctions') + revisePriorityStatic, + getNonFunctionalBeliefs, + preprocessFunctionalBeliefs } = require('../../../../src/agent/beliefRevision/revisionFunctions') const { beliefs, @@ -134,3 +137,23 @@ describe('revisionFunctions', () => { expect(newAgent.beliefs.isRaining.priority).toBe(1) }) }) + +describe('functional belief revision functions', () => { + const isRaining = Belief('isRaining', true, 1, false) + const isSlippery = FunctionalBelief( + 'isSlippery', false, (_, newBeliefs) => newBeliefs.isRaining, 0 + ) + + it('(getNonFunctionalBeliefs) should filter out functional beliefs, given a belief base', () => { + const nonFunctionalBeliefs = getNonFunctionalBeliefs({isRaining, isSlippery}) + expect(Object.keys(nonFunctionalBeliefs).length).toEqual(1) + expect(nonFunctionalBeliefs.isRaining.value).toEqual(true) + }) + + it('(preprocessFunctionalBeliefs) should filter out non-functional beliefs, given a belief base', () => { + const functionalBeliefs = preprocessFunctionalBeliefs({isRaining, isSlippery}) + expect(functionalBeliefs.length).toEqual(1) + expect(functionalBeliefs[0].value).toEqual(false) + }) + +}) diff --git a/src/agent/Agent.js b/src/agent/Agent.js index 3dbc061..b578165 100644 --- a/src/agent/Agent.js +++ b/src/agent/Agent.js @@ -1,5 +1,11 @@ const Intentions = require('./Intentions') const defaultBeliefRevisionFunction = require('./beliefRevision/revisionFunctions').reviseSimpleNonmonotonic +const { + preprocessFunctionalBeliefs, + getNonFunctionalBeliefs, + getFunctionalBeliefs, + processFunctionalBeliefs +} = require('./beliefRevision/revisionFunctions') const defaultPreferenceFunction = (beliefs, desires) => desireKey => desires[desireKey](beliefs) const defaultGoalRevisionFunction = (beliefs, goals) => goals @@ -82,7 +88,19 @@ function Agent ( } this.isActive = true this.next = function (beliefs) { - this.beliefs = this.reviseBeliefs(this.beliefs, beliefs) + // revision of non-functional beliefs + const oldBeliefs = getNonFunctionalBeliefs(this.beliefs) + this.beliefs = this.reviseBeliefs(this.beliefs, getNonFunctionalBeliefs(beliefs)) + // revision of functional beliefs + const newFunctionalBeliefs = processFunctionalBeliefs( + getFunctionalBeliefs(this.beliefs), + getFunctionalBeliefs(beliefs), + oldBeliefs, + beliefs, + reviseBeliefs + ) + this.beliefs = { ...this.beliefs, ...newFunctionalBeliefs } + // end: revision of functional beliefs this.goals = this.reviseGoals(this.beliefs, this.goals) if (this.isActive) { if (Object.keys(this.desires).length === 0) { diff --git a/src/agent/FunctionalBelief.js b/src/agent/FunctionalBelief.js new file mode 100644 index 0000000..9ae5b5e --- /dev/null +++ b/src/agent/FunctionalBelief.js @@ -0,0 +1,21 @@ +const Belief = require('./Belief') + +const warning = 'JS-son: functional belief without proper two-argument function' + +/** + * JS-son agent belief generator + * @param {string} id the belief's unique identifier + * @param {any} value the belief's default value + * @param {function} rule the function that is used to infer the belief, given the agent's current beliefs and the belief update + * @param {number} order the belief's order when belief functions are evaluated + * @param {number} priority the belief's priority in case of belief revision; optional + * @param {boolean} updatePriority whether in case of a belief update, the priority of the defeating belief should be adopted; optional, defaults to true + * @returns {object} JS-son agent functional belief + */ +const FunctionalBelief = (id, value, rule, order, priority, updatePriority=false) => { + if (typeof rule !== 'function' || rule.length !== 2) console.warn(warning) + const baseBelief = Belief(id, value, priority, updatePriority=false) + return { ...baseBelief, rule, order, value } +} + +module.exports = FunctionalBelief \ No newline at end of file diff --git a/src/agent/beliefRevision/revisionFunctions.js b/src/agent/beliefRevision/revisionFunctions.js index 07f6566..abd28e8 100644 --- a/src/agent/beliefRevision/revisionFunctions.js +++ b/src/agent/beliefRevision/revisionFunctions.js @@ -17,8 +17,7 @@ const reviseSimpleNonmonotonic = (oldBeliefs, newBeliefs) => ({ ...oldBeliefs, . const reviseMonotonic = (oldBeliefs, newBeliefs) => ({ ...newBeliefs, ...oldBeliefs }) /** - * Revises beliefs by merging old and new beliefs such that an old belief overrides a new one in - * case of conflict + * Revises beliefs by merging old and new beliefs such that an old belief overrides a new one in case of conflict. * @param {object} oldBeliefs Old belief base (JSON object of beliefs) * @param {object} newBeliefs New belief base (JSON object of beliefs) * @returns Revised belief base (JSON object of beliefs) @@ -32,7 +31,7 @@ const revisePriority = (oldBeliefs, newBeliefs) => Object.fromEntries( ) /** - * Revises beliefs by merging old and new beliefs such that an old belief overrides a new one in. Does not update belief priorities. + * Revises beliefs by merging old and new beliefs such that an old belief overrides a new one in case of conflict. Does not update belief priorities. * case of conflict * @param {object} oldBeliefs Old belief base (JSON object of beliefs) * @param {object} newBeliefs New belief base (JSON object of beliefs) @@ -48,9 +47,78 @@ const revisePriorityStatic = (oldBeliefs, newBeliefs) => revisePriority(Object.f ) ), newBeliefs) +/** + * Removes all beliefs that are functional beliefs from a belief base and returns the belief base + * by 'order' value + * @param {object} beliefs Object of beliefs + * @returns object of filtered beliefs + */ +const getNonFunctionalBeliefs = beliefs => beliefs + ? Object.fromEntries( + new Map(Object.keys(beliefs).filter( + key => !beliefs[key].rule + ).map(key => [key, beliefs[key]])) + ) : {} + +/** + * Removes all beliefs that are not functional beliefs from a belief base and returns the belief base + * by 'order' value + * @param {object} beliefs Object of beliefs + * @returns object of filtered beliefs + */ +const getFunctionalBeliefs = beliefs => beliefs + ? Object.fromEntries( + new Map(Object.keys(beliefs).filter( + key => beliefs[key].rule + ).map(key => [key, beliefs[key]])) + ) : {} + +/** + * Removes all beliefs that are not functional beliefs from a belief base and sorts the beliefs + * by 'order' value + * @param {object} beliefs Object of beliefs + * @returns array of filtered and sorted functional beliefs + */ +const preprocessFunctionalBeliefs = beliefs => Object.keys(beliefs).filter( + key => beliefs[key].rule +).map( + key => beliefs[key] +).sort((beliefA, beliefB) => beliefA.order < beliefB.order) + +/** + * Applies the functions of all functional beliefs in the provided order + * + * @param {array} oldFunctionalBeliefs Array of current functional beliefs + * @param {array} newFunctionalBeliefs Array of functional beliefs (update) + * @param {array} oldBeliefs Object of beliefs, representing the current non-functional beliefs of the agent + * @param {array} newBeliefs Object of beliefs, representing the belief update + * @param {function} reviseBeliefs General belief revision function + * @returns object of updated functional beliefs + */ +const processFunctionalBeliefs = (oldFunctionalBeliefs, newFunctionalBeliefs, oldBeliefs, newBeliefs, reviseBeliefs) => { + const beliefUpdate = reviseBeliefs(oldFunctionalBeliefs, newFunctionalBeliefs) + const sortedBeliefUpdate = Object.keys(beliefUpdate).map(key => ({ id: key, ...beliefUpdate[key] })).sort((beliefA, beliefB) => beliefA.order < beliefB.order) + return Object.fromEntries( + new Map(sortedBeliefUpdate.map(belief => [ + belief.id, + { + ...belief, + value: belief.rule( + { ...oldFunctionalBeliefs, ...oldBeliefs }, + { ...newFunctionalBeliefs, ...newBeliefs } + ) + } + ])) +) +} + module.exports = { reviseSimpleNonmonotonic, reviseMonotonic, revisePriority, - revisePriorityStatic + revisePriorityStatic, + getNonFunctionalBeliefs, + getFunctionalBeliefs, + preprocessFunctionalBeliefs, + processFunctionalBeliefs }