From 3b7aeafe5e2dbd0748aa52eec3a124fdeb6c103c Mon Sep 17 00:00:00 2001 From: Kasper Marinus Date: Wed, 29 Oct 2025 11:23:56 +0100 Subject: [PATCH 1/2] fix: correct belief management There was an issue in how we treated beliefs, specifically with multiple beliefs of the same name but different arguments. This is fixed with this commit. Also implemented correct updating of the "responded" belief, when the user_said belief is updated (when we get a new user message, we state that we have not yet responded to that message) ref: N25B-197 --- src/control_backend/agents/bdi/bdi_core.py | 17 ++++++++-- .../agents/bdi/behaviours/belief_setter.py | 31 ++++++++++------- .../bdi/behaviours/test_belief_setter.py | 34 +++++++++++-------- 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/src/control_backend/agents/bdi/bdi_core.py b/src/control_backend/agents/bdi/bdi_core.py index 1696303..d928a71 100644 --- a/src/control_backend/agents/bdi/bdi_core.py +++ b/src/control_backend/agents/bdi/bdi_core.py @@ -1,9 +1,11 @@ +import asyncio import logging import agentspeak +import spade_bdi.bdi 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): @@ -11,13 +13,12 @@ class BDICoreAgent(BDIAgent): This is the Brain agent that does the belief inference with AgentSpeak. This is a continous process that happens automatically in the background. This class contains all the actions that can be called from AgentSpeak plans. - It has the BeliefSetter behaviour. """ logger = logging.getLogger("BDI Core") async def setup(self): - belief_setter = BeliefSetter() + belief_setter = BeliefSetterBehaviour() self.add_behaviour(belief_setter) def add_custom_actions(self, actions): @@ -33,3 +34,13 @@ class BDICoreAgent(BDIAgent): def _send_to_llm(self, message) -> str: """TODO: implement""" return f"This is a reply to {message}" + + +async def main(): + bdi = BDICoreAgent("test_agent@localhost", "test_agent", "src/control_backend/agents/bdi/rules.asl") + await bdi.start() + bdi.bdi.set_belief("test", "one", "two") + print(bdi.bdi.get_beliefs()) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/src/control_backend/agents/bdi/behaviours/belief_setter.py b/src/control_backend/agents/bdi/behaviours/belief_setter.py index d36fe5e..d8324e5 100644 --- a/src/control_backend/agents/bdi/behaviours/belief_setter.py +++ b/src/control_backend/agents/bdi/behaviours/belief_setter.py @@ -1,19 +1,17 @@ -import asyncio import json import logging from spade.agent import Message 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 -class BeliefSetter(CyclicBehaviour): +class BeliefSetterBehaviour(CyclicBehaviour): """ 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 - containing beliefs from BeliefCollector and adds these to its KB. + message and processes it based on sender. """ agent: BDIAgent @@ -24,7 +22,7 @@ class BeliefSetter(CyclicBehaviour): if msg: self.logger.info(f"Received message {msg.body}") self._process_message(msg) - await asyncio.sleep(1) + def _process_message(self, message: Message): sender = message.sender.node # removes host from jid and converts to str @@ -44,19 +42,28 @@ class BeliefSetter(CyclicBehaviour): match message.thread: case "beliefs": try: - beliefs: dict[str, list[list[str]]] = json.loads(message.body) + beliefs: dict[str, list[str]] = json.loads(message.body) self._set_beliefs(beliefs) except json.JSONDecodeError as e: self.logger.error("Could not decode beliefs into JSON format: %s", e) case _: 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: self.logger.warning("Cannot set beliefs, since agent's BDI is not yet initialized.") return - for belief, arguments_list in beliefs.items(): - for arguments in arguments_list: - self.agent.bdi.set_belief(belief, *arguments) - self.logger.info("Set belief %s with arguments %s", belief, arguments) + # Set new beliefs (outdated beliefs are automatically removed) + for belief, arguments in beliefs.items(): + self.agent.bdi.set_belief(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) diff --git a/test/unit/agents/bdi/behaviours/test_belief_setter.py b/test/unit/agents/bdi/behaviours/test_belief_setter.py index b8f5570..85277da 100644 --- a/test/unit/agents/bdi/behaviours/test_belief_setter.py +++ b/test/unit/agents/bdi/behaviours/test_belief_setter.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, call 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 COLLECTOR_AGENT_NAME = "belief_collector" @@ -22,16 +22,14 @@ def mock_agent(mocker): @pytest.fixture 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 mocker.patch( "control_backend.agents.bdi.behaviours.belief_setter.settings.agent_settings.belief_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 # Mock the receive method, we will control its return value in each test 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. """ # Arrange - beliefs_payload = {"is_hot": [["kitchen"]], "is_clean": [["kitchen"], ["bathroom"]]} + beliefs_payload = {"is_hot": ["kitchen"], "is_clean": ["kitchen", "bathroom"]} msg = create_mock_message( 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 beliefs_to_set = { - "is_hot": [["kitchen"], ["living_room"]], - "door_is": [["front_door", "closed"]], + "is_hot": ["kitchen"], + "door_opened": ["front_door", "back_door"], } # Act @@ -196,17 +194,25 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog): # Assert expected_calls = [ call("is_hot", "kitchen"), - call("is_hot", "living_room"), - call("door_is", "front_door", "closed"), + call("door_opened", "front_door", "back_door"), ] 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 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_is with arguments ['front_door', 'closed']" in caplog.text + assert "Set belief door_opened with arguments ['front_door', 'back_door']" 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): """ @@ -214,7 +220,7 @@ def test_set_beliefs_bdi_not_initialized(belief_setter, mock_agent, caplog): """ # Arrange mock_agent.bdi = None # Simulate BDI not being ready - beliefs_to_set = {"is_hot": [["kitchen"]]} + beliefs_to_set = {"is_hot": ["kitchen"]} # Act with caplog.at_level(logging.WARNING): From 2da02946edc9056f53554d879c858fbc634b5419 Mon Sep 17 00:00:00 2001 From: Kasper Marinus Date: Wed, 29 Oct 2025 13:21:55 +0100 Subject: [PATCH 2/2] chore: remove manual testing function ref: N25B-197 --- src/control_backend/agents/bdi/bdi_core.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/control_backend/agents/bdi/bdi_core.py b/src/control_backend/agents/bdi/bdi_core.py index d928a71..7908507 100644 --- a/src/control_backend/agents/bdi/bdi_core.py +++ b/src/control_backend/agents/bdi/bdi_core.py @@ -33,14 +33,4 @@ class BDICoreAgent(BDIAgent): def _send_to_llm(self, message) -> str: """TODO: implement""" - return f"This is a reply to {message}" - - -async def main(): - bdi = BDICoreAgent("test_agent@localhost", "test_agent", "src/control_backend/agents/bdi/rules.asl") - await bdi.start() - bdi.bdi.set_belief("test", "one", "two") - print(bdi.bdi.get_beliefs()) - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file + return f"This is a reply to {message}" \ No newline at end of file