From c7428447163fb6ad74ec6ae2fd709c355b433046 Mon Sep 17 00:00:00 2001 From: Timotheus Kampik Date: Sun, 5 May 2024 21:06:19 +0200 Subject: [PATCH] Add out-of-the-box belief revision functions Closes #236 Implements a naive/monotonic and a priority rule-based revision function --- README.md | 46 +++++++- doc/api.md | 12 +++ spec/src/agent/Agent.spec.js | 4 +- spec/src/agent/Belief.spec.js | 4 + .../beliefRevision/revisionFunctions.spec.js | 101 ++++++++++++++++++ src/agent/Agent.js | 2 +- src/agent/Belief.js | 9 +- src/agent/beliefRevision/revisionFunctions.js | 40 +++++++ src/js-son.js | 3 +- 9 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 spec/src/agent/beliefRevision/revisionFunctions.spec.js create mode 100644 src/agent/beliefRevision/revisionFunctions.js diff --git a/README.md b/README.md index c4b6843..cd9b087 100644 --- a/README.md +++ b/README.md @@ -562,7 +562,7 @@ However, JS-son supports the implementation of a custom belief revision function For example, let us implement the following simple agent: ```JavaScript -let agent = new Agent('myAgent', { ...Belief('a', true) }, {}, []) +const agent = new Agent('myAgent', { ...Belief('a', true) }, {}, []) ``` Now, let us run the agent so that the environment changes the agent's belief about ``a``. @@ -581,7 +581,7 @@ const (oldBeliefs, newBeliefs) => ({ ...newBeliefs, a: true }) -let agent = new Agent('myAgent', { ...Belief('a', true) }, {}, [], undefined, false, reviseBeliefs) +const agent = new Agent('myAgent', { ...Belief('a', true) }, {}, [], undefined, false, reviseBeliefs) ``` To test the change, proceed as follows: @@ -592,6 +592,48 @@ agent.next({ ...Belief('a', false) }) ``agent.beliefs.a`` is ``true``. +JS-son provides an out-of-the-box belief revision function that handles priority rules. +We can import this function as follows: + +```JavaScript +const revisePriority = JSson.revisionFunctions.revisePriority +``` + +Let us now specify an initial belief base and an update thereof. +In both, each belief has a numerical priority value: + +```JavaScript +const beliefBase = { + isRaining: Belief('isRaining', true, 0), + temperature: Belief('temperature', 10, 0), + propertyValue: Belief('propertyValue', 500000, 1) +} + +const update = { + isRaining: Belief('isRaining', false, 0), + temperature: Belief('temperature', 15, 1), + propertyValue: Belief('propertyValue', 250000, 0) +} + +const agent = new Agent('myAgent', beliefBase, {}, [], undefined, false, revisePriority) +``` + +After applying the belief update, our agent's belief base is as follows: + +```JavaScript +{ + isRaining: Belief('isRaining', false, 0), + temperature: Belief('temperature', 15, 1), + propertyValue: Belief('propertyValue', 500000, 1) +} +``` + +Note that in detail, the priorities are interpreted as follows: + +* If a belief exists in the update, but not in the agent's belief base, this belief is added. +* If the belief's priority is 0 in the belief base and a belief with the same key exists in the update, the agent's belief is overridden; this behavior is desired for beliefs that are generally defeasible. +* If a belief's priority in the update is higher than the same belief's priority in the agent's belief base, the agent's belief is overridden. + ## 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/doc/api.md b/doc/api.md index c993ee5..b03d9b2 100644 --- a/doc/api.md +++ b/doc/api.md @@ -18,6 +18,18 @@ ``` +## Built-In Belief Revision Functions + +```eval_rst + +.. js:autofunction:: reviseSimpleNonmonotonic + +.. js:autofunction:: reviseMonotonic + +.. js:autofunction:: revisePriority + +``` + ## Environment ```eval_rst diff --git a/spec/src/agent/Agent.spec.js b/spec/src/agent/Agent.spec.js index f9d006f..957cb1c 100644 --- a/spec/src/agent/Agent.spec.js +++ b/spec/src/agent/Agent.spec.js @@ -71,7 +71,7 @@ describe('Agent / next()', () => { } )) const newAgent = new Agent('myAgent', beliefs, desires, newPlans, preferenceFunctionGen, false) - expect(() => newAgent.next()).toThrow(new TypeError("Cannot set property 'test' of undefined")) + expect(() => newAgent.next()).toThrow(new TypeError("Cannot set properties of undefined (setting 'test')")) }) it('should allow for a custom belief revision function that rejects belief updates from the environment', () => { @@ -167,7 +167,7 @@ describe('Agent / next(), configuration object-based', () => { determinePreferences: preferenceFunctionGen, selfUpdatesPossible: false }) - expect(() => newAgent.next()).toThrow(new TypeError("Cannot set property 'test' of undefined")) + expect(() => newAgent.next()).toThrow(new TypeError("Cannot set properties of undefined (setting 'test')")) }) it('should support a custom belief revision function that rejects belief updates from the environment', () => { diff --git a/spec/src/agent/Belief.spec.js b/spec/src/agent/Belief.spec.js index 5fdd80b..1d4ef1d 100644 --- a/spec/src/agent/Belief.spec.js +++ b/spec/src/agent/Belief.spec.js @@ -9,6 +9,10 @@ describe('belief()', () => { expect(Belief('test', 'test')).toEqual({ test: 'test' }) }) + it('should create a new belief with the specified key, value (explicitly managed), and priority', () => { + expect(Belief('test', 'test', 1)).toEqual({ test: 'test', value: 'test', priority: 1 }) + }) + it('should not throw a warning if belief is of a JSON data type', () => { console.warn.calls.reset() // eslint-disable-next-line no-unused-vars diff --git a/spec/src/agent/beliefRevision/revisionFunctions.spec.js b/spec/src/agent/beliefRevision/revisionFunctions.spec.js new file mode 100644 index 0000000..f344ad2 --- /dev/null +++ b/spec/src/agent/beliefRevision/revisionFunctions.spec.js @@ -0,0 +1,101 @@ +const Agent = require('../../../../src/agent/Agent') +const Belief = require('../../../../src/agent/Belief') +const { + reviseSimpleNonmonotonic, + reviseMonotonic, + revisePriority } = require('../../../../src/agent/beliefRevision/revisionFunctions') + +const { + beliefs, + desires, + plans +} = require('../../../mocks/human') + +describe('revisionFunctions', () => { + + it('by default, the belief revision function is simple non-monotonic and updates existing beliefs', () => { + const newAgent = new Agent({ + id: 'myAgent', + beliefs, + desires, + plans, + selfUpdatesPossible: false + }) + newAgent.next({ ...Belief('dogNice', false) }) + + const alternativeAgent = new Agent({ + id: 'alternativeAgent', + beliefs, + desires, + plans, + selfUpdatesPossible: false, + reviseBeliefs: reviseSimpleNonmonotonic + }) + alternativeAgent.next({ ...Belief('dogNice', false) }) + expect(alternativeAgent.beliefs.dogNice).toBe(false) + expect(alternativeAgent.beliefs.dogNice).toEqual(newAgent.beliefs.dogNice) + }) + + it('should support a monotonic belief revision function that rejects updates to existing beliefs', () => { + const newAgent = new Agent({ + id: 'myAgent', + beliefs, + desires, + plans, + selfUpdatesPossible: false, + reviseBeliefs: reviseMonotonic + }) + newAgent.next({ ...Belief('dogNice', false), ...Belief('weather', 'sunny') }) + expect(newAgent.beliefs.dogNice).toBe(true) + expect(newAgent.beliefs.weather).toBe('sunny') + }) + + it('should support a nonmonotonic belief revision function based on priority rules', () => { + const beliefBase = { + isRaining: Belief('isRaining', true, 0), + temperature: Belief('temperature', 10, 0), + propertyValue: Belief('propertyValue', 500000, 1) + } + + const update = { + isRaining: Belief('isRaining', false, 0), + temperature: Belief('temperature', 15, 1), + propertyValue: Belief('propertyValue', 250000, 0) + } + const newAgent = new Agent({ + id: 'myAgent', + beliefs: beliefBase, + desires, + plans, + selfUpdatesPossible: false, + reviseBeliefs: revisePriority + }) + newAgent.next(update) + expect(newAgent.beliefs.isRaining.value).toBe(false) + expect(newAgent.beliefs.temperature.value).toEqual(15) + expect(newAgent.beliefs.propertyValue.value).toEqual(500000) + }) + + it('should not allow overriding beliefs of infinite high priority', () => { + const beliefBase = { + isRaining: Belief('isRaining', true, Infinity), + temperature: Belief('temperature', 10, Infinity) + } + + const update = { + isRaining: Belief('isRaining', false, Infinity), + temperature: Belief('temperature', 15, undefined) + } + const newAgent = new Agent({ + id: 'myAgent', + beliefs: beliefBase, + desires, + plans, + selfUpdatesPossible: false, + reviseBeliefs: revisePriority + }) + newAgent.next(update) + expect(newAgent.beliefs.isRaining.value).toBe(true) + expect(newAgent.beliefs.temperature.value).toEqual(10) + }) +}) diff --git a/src/agent/Agent.js b/src/agent/Agent.js index 7cf67e2..3dbc061 100644 --- a/src/agent/Agent.js +++ b/src/agent/Agent.js @@ -1,7 +1,7 @@ const Intentions = require('./Intentions') +const defaultBeliefRevisionFunction = require('./beliefRevision/revisionFunctions').reviseSimpleNonmonotonic const defaultPreferenceFunction = (beliefs, desires) => desireKey => desires[desireKey](beliefs) -const defaultBeliefRevisionFunction = (oldBeliefs, newBeliefs) => ({ ...oldBeliefs, ...newBeliefs }) const defaultGoalRevisionFunction = (beliefs, goals) => goals /** diff --git a/src/agent/Belief.js b/src/agent/Belief.js index 307260e..261e774 100644 --- a/src/agent/Belief.js +++ b/src/agent/Belief.js @@ -3,12 +3,17 @@ const warning = 'JS-son: Created belief with non-JSON object, non-JSON data type /** * JS-son agent belief generator * @param {string} id the belief's unique identifier - * @param {any} value + * @param {any} value the belief's value + * @param {number} priority the belief's priority in case of belief revision; optional * @returns {object} JS-son agent belief */ -const Belief = (id, value) => { +const Belief = (id, value, priority) => { const belief = {} belief[id] = value + if (priority || priority === 0) { + belief.priority = priority + belief['value'] = value + } try { const parsedBelief = JSON.parse(JSON.stringify(belief)) if (Object.keys(parsedBelief).length !== Object.keys(belief).length) { diff --git a/src/agent/beliefRevision/revisionFunctions.js b/src/agent/beliefRevision/revisionFunctions.js new file mode 100644 index 0000000..e20346e --- /dev/null +++ b/src/agent/beliefRevision/revisionFunctions.js @@ -0,0 +1,40 @@ +/** + * Revises beliefs by merging old and new beliefs such that a new belief overwrites an old 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) + */ +const reviseSimpleNonmonotonic = (oldBeliefs, newBeliefs) => ({ ...oldBeliefs, ...newBeliefs }) + +/** + * 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) + */ +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 + * @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) + */ +const revisePriority = (oldBeliefs, newBeliefs) => { + const update = oldBeliefs + Object.keys(newBeliefs).forEach(key => { + if(!key in oldBeliefs || oldBeliefs[key].priority == 0 || oldBeliefs[key].priority < newBeliefs[key].priority) { + update[key] = newBeliefs[key] + } + }) + return update +} + +module.exports = { + reviseSimpleNonmonotonic, + reviseMonotonic, + revisePriority +} diff --git a/src/js-son.js b/src/js-son.js index d2d507c..753ab03 100644 --- a/src/js-son.js +++ b/src/js-son.js @@ -8,7 +8,8 @@ const JSson = { RemoteAgent: require('./agent/RemoteAgent'), Environment: require('./environment/Environment'), GridWorld: require('./environment/GridWorld'), - FieldType: require('./environment/FieldType') + FieldType: require('./environment/FieldType'), + revisionFunctions: require('./agent/beliefRevision/revisionFunctions') } module.exports = JSson