diff --git a/src/control_backend/agents/bdi/bdi_core.py b/src/control_backend/agents/bdi/bdi_core.py index 859e25a..7f424cc 100644 --- a/src/control_backend/agents/bdi/bdi_core.py +++ b/src/control_backend/agents/bdi/bdi_core.py @@ -1,14 +1,15 @@ +import asyncio import logging import agentspeak +import spade_bdi.bdi from spade.behaviour import OneShotBehaviour from spade.message import Message from spade_bdi.bdi import BDIAgent -from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter -from control_backend.agents.bdi.behaviours.receive_llm_resp_behaviour import ( - ReceiveLLMResponseBehaviour, -) +from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetterBehaviour +from control_backend.agents.bdi.behaviours.receive_llm_resp_behaviour import ReceiveLLMResponseBehaviour + from control_backend.core.config import settings @@ -28,7 +29,7 @@ class BDICoreAgent(BDIAgent): """ self.logger.info("BDICoreAgent setup started") - self.add_behaviour(BeliefSetter()) + self.add_behaviour(BeliefSetterBehaviour()) self.add_behaviour(ReceiveLLMResponseBehaviour()) await self._send_to_llm("Hi pepper, how are you?") diff --git a/src/control_backend/agents/bdi/behaviours/belief_setter.py b/src/control_backend/agents/bdi/behaviours/belief_setter.py index 157986b..6179052 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 @@ -45,19 +43,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):