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:
@@ -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):
|
||||||
@@ -32,4 +33,4 @@ class BDICoreAgent(BDIAgent):
|
|||||||
|
|
||||||
def _send_to_llm(self, message) -> str:
|
def _send_to_llm(self, message) -> str:
|
||||||
"""TODO: implement"""
|
"""TODO: implement"""
|
||||||
return f"This is a reply to {message}"
|
return f"This is a reply to {message}"
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user