Merge branch 'fix/bdi-correct-belief-management' into 'dev'

Fix belief management in BDI

See merge request ics/sp/2025/n25b/pepperplus-cb!11
This commit was merged in pull request #11.
This commit is contained in:
Twirre
2025-10-29 12:36:03 +00:00
3 changed files with 44 additions and 30 deletions

View File

@@ -1,9 +1,11 @@
import asyncio
import logging import logging
import agentspeak import agentspeak
import spade_bdi.bdi
from spade_bdi.bdi import BDIAgent from spade_bdi.bdi import BDIAgent
from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetterBehaviour
class BDICoreAgent(BDIAgent): class BDICoreAgent(BDIAgent):
@@ -11,13 +13,12 @@ class BDICoreAgent(BDIAgent):
This is the Brain agent that does the belief inference with AgentSpeak. This is the Brain agent that does the belief inference with AgentSpeak.
This is a continous process that happens automatically in the background. This is a continous process that happens automatically in the background.
This class contains all the actions that can be called from AgentSpeak plans. This class contains all the actions that can be called from AgentSpeak plans.
It has the BeliefSetter behaviour.
""" """
logger = logging.getLogger("BDI Core") logger = logging.getLogger("BDI Core")
async def setup(self): async def setup(self):
belief_setter = BeliefSetter() belief_setter = BeliefSetterBehaviour()
self.add_behaviour(belief_setter) self.add_behaviour(belief_setter)
def add_custom_actions(self, actions): def add_custom_actions(self, actions):

View File

@@ -1,19 +1,17 @@
import asyncio
import json import json
import logging import logging
from spade.agent import Message from spade.agent import Message
from spade.behaviour import CyclicBehaviour from spade.behaviour import CyclicBehaviour
from spade_bdi.bdi import BDIAgent from spade_bdi.bdi import BDIAgent, BeliefNotInitiated
from control_backend.core.config import settings from control_backend.core.config import settings
class BeliefSetter(CyclicBehaviour): class BeliefSetterBehaviour(CyclicBehaviour):
""" """
This is the behaviour that the BDI agent runs. This behaviour waits for incoming This is the behaviour that the BDI agent runs. This behaviour waits for incoming
message and processes it based on sender. Currently, it only waits for messages message and processes it based on sender.
containing beliefs from BeliefCollector and adds these to its KB.
""" """
agent: BDIAgent agent: BDIAgent
@@ -24,7 +22,7 @@ class BeliefSetter(CyclicBehaviour):
if msg: if msg:
self.logger.info(f"Received message {msg.body}") self.logger.info(f"Received message {msg.body}")
self._process_message(msg) self._process_message(msg)
await asyncio.sleep(1)
def _process_message(self, message: Message): def _process_message(self, message: Message):
sender = message.sender.node # removes host from jid and converts to str sender = message.sender.node # removes host from jid and converts to str
@@ -44,19 +42,28 @@ class BeliefSetter(CyclicBehaviour):
match message.thread: match message.thread:
case "beliefs": case "beliefs":
try: try:
beliefs: dict[str, list[list[str]]] = json.loads(message.body) beliefs: dict[str, list[str]] = json.loads(message.body)
self._set_beliefs(beliefs) self._set_beliefs(beliefs)
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
self.logger.error("Could not decode beliefs into JSON format: %s", e) self.logger.error("Could not decode beliefs into JSON format: %s", e)
case _: case _:
pass pass
def _set_beliefs(self, beliefs: dict[str, list[list[str]]]): def _set_beliefs(self, beliefs: dict[str, list[str]]):
"""Remove previous values for beliefs and update them with the provided values."""
if self.agent.bdi is None: if self.agent.bdi is None:
self.logger.warning("Cannot set beliefs, since agent's BDI is not yet initialized.") self.logger.warning("Cannot set beliefs, since agent's BDI is not yet initialized.")
return return
for belief, arguments_list in beliefs.items(): # Set new beliefs (outdated beliefs are automatically removed)
for arguments in arguments_list: for belief, arguments in beliefs.items():
self.agent.bdi.set_belief(belief, *arguments) self.agent.bdi.set_belief(belief, *arguments)
self.logger.info("Set belief %s with arguments %s", belief, arguments)
# Special case: if there's a new user message, we need to flag that we haven't responded yet
if belief == "user_said":
try:
self.agent.bdi.remove_belief("responded")
except BeliefNotInitiated:
pass
self.logger.info("Set belief %s with arguments %s", belief, arguments)

View File

@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, call
import pytest import pytest
from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetterBehaviour
# Define a constant for the collector agent name to use in tests # Define a constant for the collector agent name to use in tests
COLLECTOR_AGENT_NAME = "belief_collector" COLLECTOR_AGENT_NAME = "belief_collector"
@@ -22,16 +22,14 @@ def mock_agent(mocker):
@pytest.fixture @pytest.fixture
def belief_setter(mock_agent, mocker): def belief_setter(mock_agent, mocker):
"""Fixture to create an instance of BeliefSetter with a mocked agent.""" """Fixture to create an instance of BeliefSetterBehaviour with a mocked agent."""
# Patch the settings to use a predictable agent name # Patch the settings to use a predictable agent name
mocker.patch( mocker.patch(
"control_backend.agents.bdi.behaviours.belief_setter.settings.agent_settings.belief_collector_agent_name", "control_backend.agents.bdi.behaviours.belief_setter.settings.agent_settings.belief_collector_agent_name",
COLLECTOR_AGENT_NAME, COLLECTOR_AGENT_NAME,
) )
# Patch asyncio.sleep to prevent tests from actually waiting
mocker.patch("asyncio.sleep", return_value=None)
setter = BeliefSetter() setter = BeliefSetterBehaviour()
setter.agent = mock_agent setter.agent = mock_agent
# Mock the receive method, we will control its return value in each test # Mock the receive method, we will control its return value in each test
setter.receive = AsyncMock() setter.receive = AsyncMock()
@@ -115,7 +113,7 @@ def test_process_belief_message_valid_json(belief_setter, mocker):
Test processing a valid belief message with correct thread and JSON body. Test processing a valid belief message with correct thread and JSON body.
""" """
# Arrange # Arrange
beliefs_payload = {"is_hot": [["kitchen"]], "is_clean": [["kitchen"], ["bathroom"]]} beliefs_payload = {"is_hot": ["kitchen"], "is_clean": ["kitchen", "bathroom"]}
msg = create_mock_message( msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID, body=json.dumps(beliefs_payload), thread="beliefs" sender_node=COLLECTOR_AGENT_JID, body=json.dumps(beliefs_payload), thread="beliefs"
) )
@@ -185,8 +183,8 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
""" """
# Arrange # Arrange
beliefs_to_set = { beliefs_to_set = {
"is_hot": [["kitchen"], ["living_room"]], "is_hot": ["kitchen"],
"door_is": [["front_door", "closed"]], "door_opened": ["front_door", "back_door"],
} }
# Act # Act
@@ -196,17 +194,25 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
# Assert # Assert
expected_calls = [ expected_calls = [
call("is_hot", "kitchen"), call("is_hot", "kitchen"),
call("is_hot", "living_room"), call("door_opened", "front_door", "back_door"),
call("door_is", "front_door", "closed"),
] ]
mock_agent.bdi.set_belief.assert_has_calls(expected_calls, any_order=True) mock_agent.bdi.set_belief.assert_has_calls(expected_calls, any_order=True)
assert mock_agent.bdi.set_belief.call_count == 3 assert mock_agent.bdi.set_belief.call_count == 2
# Check logs # Check logs
assert "Set belief is_hot with arguments ['kitchen']" in caplog.text assert "Set belief is_hot with arguments ['kitchen']" in caplog.text
assert "Set belief is_hot with arguments ['living_room']" in caplog.text assert "Set belief door_opened with arguments ['front_door', 'back_door']" in caplog.text
assert "Set belief door_is with arguments ['front_door', 'closed']" in caplog.text
def test_responded_unset(belief_setter, mock_agent):
# Arrange
new_beliefs = {"user_said": ["message"]}
# Act
belief_setter._set_beliefs(new_beliefs)
# Assert
mock_agent.bdi.set_belief.assert_has_calls([call("user_said", "message")])
mock_agent.bdi.remove_belief.assert_has_calls([call("responded")])
def test_set_beliefs_bdi_not_initialized(belief_setter, mock_agent, caplog): def test_set_beliefs_bdi_not_initialized(belief_setter, mock_agent, caplog):
""" """
@@ -214,7 +220,7 @@ def test_set_beliefs_bdi_not_initialized(belief_setter, mock_agent, caplog):
""" """
# Arrange # Arrange
mock_agent.bdi = None # Simulate BDI not being ready mock_agent.bdi = None # Simulate BDI not being ready
beliefs_to_set = {"is_hot": [["kitchen"]]} beliefs_to_set = {"is_hot": ["kitchen"]}
# Act # Act
with caplog.at_level(logging.WARNING): with caplog.at_level(logging.WARNING):