Merge remote-tracking branch 'origin/dev' into refactor/config-file

# Conflicts:
#	src/control_backend/agents/ri_communication_agent.py
#	src/control_backend/core/config.py
#	src/control_backend/main.py
This commit is contained in:
Twirre Meulenbelt
2025-11-19 17:30:48 +01:00
46 changed files with 1207 additions and 651 deletions

View File

@@ -4,10 +4,12 @@ from unittest.mock import AsyncMock, MagicMock, call
import pytest
from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetterBehaviour
from control_backend.agents.bdi.bdi_core_agent.behaviours.belief_setter_behaviour import (
BeliefSetterBehaviour,
)
# Define a constant for the collector agent name to use in tests
COLLECTOR_AGENT_NAME = "belief_collector"
COLLECTOR_AGENT_NAME = "belief_collector_agent"
COLLECTOR_AGENT_JID = f"{COLLECTOR_AGENT_NAME}@test"
@@ -21,11 +23,12 @@ def mock_agent(mocker):
@pytest.fixture
def belief_setter(mock_agent, mocker):
def belief_setter_behaviour(mock_agent, mocker):
"""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",
"control_backend.agents.bdi.bdi_core_agent."
"behaviours.belief_setter_behaviour.settings.agent_settings.bdi_belief_collector_name",
COLLECTOR_AGENT_NAME,
)
@@ -46,53 +49,53 @@ def create_mock_message(sender_node: str, body: str, thread: str) -> MagicMock:
@pytest.mark.asyncio
async def test_run_message_received(belief_setter, mocker):
async def test_run_message_received(belief_setter_behaviour, mocker):
"""
Test that when a message is received, _process_message is called.
"""
# Arrange
msg = MagicMock()
belief_setter.receive.return_value = msg
mocker.patch.object(belief_setter, "_process_message")
belief_setter_behaviour.receive.return_value = msg
mocker.patch.object(belief_setter_behaviour, "_process_message")
# Act
await belief_setter.run()
await belief_setter_behaviour.run()
# Assert
belief_setter._process_message.assert_called_once_with(msg)
belief_setter_behaviour._process_message.assert_called_once_with(msg)
def test_process_message_from_belief_collector(belief_setter, mocker):
def test_process_message_from_bdi_belief_collector_agent(belief_setter_behaviour, mocker):
"""
Test processing a message from the correct belief collector agent.
"""
# Arrange
msg = create_mock_message(sender_node=COLLECTOR_AGENT_NAME, body="", thread="")
mock_process_belief = mocker.patch.object(belief_setter, "_process_belief_message")
mock_process_belief = mocker.patch.object(belief_setter_behaviour, "_process_belief_message")
# Act
belief_setter._process_message(msg)
belief_setter_behaviour._process_message(msg)
# Assert
mock_process_belief.assert_called_once_with(msg)
def test_process_message_from_other_agent(belief_setter, mocker):
def test_process_message_from_other_agent(belief_setter_behaviour, mocker):
"""
Test that messages from other agents are ignored.
"""
# Arrange
msg = create_mock_message(sender_node="other_agent", body="", thread="")
mock_process_belief = mocker.patch.object(belief_setter, "_process_belief_message")
mock_process_belief = mocker.patch.object(belief_setter_behaviour, "_process_belief_message")
# Act
belief_setter._process_message(msg)
belief_setter_behaviour._process_message(msg)
# Assert
mock_process_belief.assert_not_called()
def test_process_belief_message_valid_json(belief_setter, mocker):
def test_process_belief_message_valid_json(belief_setter_behaviour, mocker):
"""
Test processing a valid belief message with correct thread and JSON body.
"""
@@ -101,16 +104,16 @@ def test_process_belief_message_valid_json(belief_setter, mocker):
msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID, body=json.dumps(beliefs_payload), thread="beliefs"
)
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs")
# Act
belief_setter._process_belief_message(msg)
belief_setter_behaviour._process_belief_message(msg)
# Assert
mock_set_beliefs.assert_called_once_with(beliefs_payload)
def test_process_belief_message_invalid_json(belief_setter, mocker, caplog):
def test_process_belief_message_invalid_json(belief_setter_behaviour, mocker, caplog):
"""
Test that a message with invalid JSON is handled gracefully and an error is logged.
"""
@@ -118,16 +121,16 @@ def test_process_belief_message_invalid_json(belief_setter, mocker, caplog):
msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID, body="this is not a json string", thread="beliefs"
)
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs")
# Act
belief_setter._process_belief_message(msg)
belief_setter_behaviour._process_belief_message(msg)
# Assert
mock_set_beliefs.assert_not_called()
def test_process_belief_message_wrong_thread(belief_setter, mocker):
def test_process_belief_message_wrong_thread(belief_setter_behaviour, mocker):
"""
Test that a message with an incorrect thread is ignored.
"""
@@ -135,31 +138,31 @@ def test_process_belief_message_wrong_thread(belief_setter, mocker):
msg = create_mock_message(
sender_node=COLLECTOR_AGENT_JID, body='{"some": "data"}', thread="not_beliefs"
)
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs")
# Act
belief_setter._process_belief_message(msg)
belief_setter_behaviour._process_belief_message(msg)
# Assert
mock_set_beliefs.assert_not_called()
def test_process_belief_message_empty_body(belief_setter, mocker):
def test_process_belief_message_empty_body(belief_setter_behaviour, mocker):
"""
Test that a message with an empty body is ignored.
"""
# Arrange
msg = create_mock_message(sender_node=COLLECTOR_AGENT_JID, body="", thread="beliefs")
mock_set_beliefs = mocker.patch.object(belief_setter, "_set_beliefs")
mock_set_beliefs = mocker.patch.object(belief_setter_behaviour, "_set_beliefs")
# Act
belief_setter._process_belief_message(msg)
belief_setter_behaviour._process_belief_message(msg)
# Assert
mock_set_beliefs.assert_not_called()
def test_set_beliefs_success(belief_setter, mock_agent, caplog):
def test_set_beliefs_success(belief_setter_behaviour, mock_agent, caplog):
"""
Test that beliefs are correctly set on the agent's BDI.
"""
@@ -171,7 +174,7 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
# Act
with caplog.at_level(logging.INFO):
belief_setter._set_beliefs(beliefs_to_set)
belief_setter_behaviour._set_beliefs(beliefs_to_set)
# Assert
expected_calls = [
@@ -182,18 +185,18 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
assert mock_agent.bdi.set_belief.call_count == 2
# def test_responded_unset(belief_setter, mock_agent):
# def test_responded_unset(belief_setter_behaviour, mock_agent):
# # Arrange
# new_beliefs = {"user_said": ["message"]}
#
# # Act
# belief_setter._set_beliefs(new_beliefs)
# belief_setter_behaviour._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_behaviour, mock_agent, caplog):
# """
# Test that a warning is logged if the agent's BDI is not initialized.
# """
@@ -203,7 +206,7 @@ def test_set_beliefs_success(belief_setter, mock_agent, caplog):
#
# # Act
# with caplog.at_level(logging.WARNING):
# belief_setter._set_beliefs(beliefs_to_set)
# belief_setter_behaviour._set_beliefs(beliefs_to_set)
#
# # Assert
# assert "Cannot set beliefs, since agent's BDI is not yet initialized." in caplog.text

View File

@@ -0,0 +1,101 @@
import json
from unittest.mock import AsyncMock, MagicMock
import pytest
from control_backend.agents.bdi.belief_collector_agent.behaviours.belief_collector_behaviour import ( # noqa: E501
BeliefCollectorBehaviour,
)
def create_mock_message(sender_node: str, body: str) -> MagicMock:
"""Helper function to create a configured mock message."""
msg = MagicMock()
msg.sender.node = sender_node # MagicMock automatically creates nested mocks
msg.body = body
return msg
@pytest.fixture
def mock_agent(mocker):
"""Fixture to create a mock Agent."""
agent = MagicMock()
agent.jid = "belief_collector_agent@test"
return agent
@pytest.fixture
def bel_collector_behaviouror(mock_agent, mocker):
"""Fixture to create an instance of BelCollectorBehaviour with a mocked agent."""
# Patch asyncio.sleep to prevent tests from actually waiting
mocker.patch("asyncio.sleep", return_value=None)
collector = BeliefCollectorBehaviour()
collector.agent = mock_agent
# Mock the receive method, we will control its return value in each test
collector.receive = AsyncMock()
return collector
@pytest.mark.asyncio
async def test_run_message_received(bel_collector_behaviouror, mocker):
"""
Test that when a message is received, _process_message is called with that message.
"""
# Arrange
mock_msg = MagicMock()
bel_collector_behaviouror.receive.return_value = mock_msg
mocker.patch.object(bel_collector_behaviouror, "_process_message")
# Act
await bel_collector_behaviouror.run()
# Assert
bel_collector_behaviouror._process_message.assert_awaited_once_with(mock_msg)
@pytest.mark.asyncio
async def test_routes_to_handle_belief_text_by_type(bel_collector_behaviouror, mocker):
msg = create_mock_message(
"anyone",
json.dumps({"type": "belief_extraction_text", "beliefs": {"user_said": [["hi"]]}}),
)
spy = mocker.patch.object(bel_collector_behaviouror, "_handle_belief_text", new=AsyncMock())
await bel_collector_behaviouror._process_message(msg)
spy.assert_awaited_once()
@pytest.mark.asyncio
async def test_routes_to_handle_belief_text_by_sender(bel_collector_behaviouror, mocker):
msg = create_mock_message(
"bel_text_agent_mock", json.dumps({"beliefs": {"user_said": [["hi"]]}})
)
spy = mocker.patch.object(bel_collector_behaviouror, "_handle_belief_text", new=AsyncMock())
await bel_collector_behaviouror._process_message(msg)
spy.assert_awaited_once()
@pytest.mark.asyncio
async def test_routes_to_handle_emo_text(bel_collector_behaviouror, mocker):
msg = create_mock_message("anyone", json.dumps({"type": "emotion_extraction_text"}))
spy = mocker.patch.object(bel_collector_behaviouror, "_handle_emo_text", new=AsyncMock())
await bel_collector_behaviouror._process_message(msg)
spy.assert_awaited_once()
@pytest.mark.asyncio
async def test_belief_text_happy_path_sends(bel_collector_behaviouror, mocker):
payload = {"type": "belief_extraction_text", "beliefs": {"user_said": ["hello test", "No"]}}
bel_collector_behaviouror.send = AsyncMock()
await bel_collector_behaviouror._handle_belief_text(payload, "bel_text_agent_mock")
# make sure we attempted a send
bel_collector_behaviouror.send.assert_awaited_once()
@pytest.mark.asyncio
async def test_belief_text_coerces_non_strings(bel_collector_behaviouror, mocker):
payload = {"type": "belief_extraction_text", "beliefs": {"user_said": [["hi", 123]]}}
bel_collector_behaviouror.send = AsyncMock()
await bel_collector_behaviouror._handle_belief_text(payload, "origin")
bel_collector_behaviouror.send.assert_awaited_once()

View File

@@ -4,7 +4,9 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from spade.message import Message
from control_backend.agents.bdi.behaviours.text_belief_extractor import BeliefFromText
from control_backend.agents.bdi.text_belief_extractor_agent.behaviours.text_belief_extractor_behaviour import ( # noqa: E501, We can't shorten this import.
TextBeliefExtractorBehaviour,
)
@pytest.fixture
@@ -15,15 +17,17 @@ def mock_settings():
"""
# Create a mock object that mimics the nested structure
settings_mock = MagicMock()
settings_mock.agent_settings.transcription_agent_name = "transcriber"
settings_mock.agent_settings.belief_collector_agent_name = "collector"
settings_mock.agent_settings.transcription_name = "transcriber"
settings_mock.agent_settings.bdi_belief_collector_name = "collector"
settings_mock.agent_settings.host = "fake.host"
# Use patch to replace the settings object during the test
# Adjust 'control_backend.behaviours.belief_from_text.settings' to where
# your behaviour file imports it from.
with patch(
"control_backend.agents.bdi.behaviours.text_belief_extractor.settings", settings_mock
"control_backend.agents.bdi.text_belief_extractor_agent.behaviours"
".text_belief_extractor_behaviour.settings",
settings_mock,
):
yield settings_mock
@@ -31,10 +35,10 @@ def mock_settings():
@pytest.fixture
def behavior(mock_settings):
"""
Creates an instance of the BeliefFromText behaviour and mocks its
Creates an instance of the BDITextBeliefBehaviour behaviour and mocks its
agent, logger, send, and receive methods.
"""
b = BeliefFromText()
b = TextBeliefExtractorBehaviour()
b.agent = MagicMock()
b.send = AsyncMock()
@@ -100,7 +104,7 @@ async def test_run_message_from_transcriber_demo(behavior, mock_settings, monkey
# Arrange: Create a mock message from the transcriber
transcription_text = "hello world"
mock_msg = create_mock_message(
mock_settings.agent_settings.transcription_agent_name, transcription_text, None
mock_settings.agent_settings.transcription_name, transcription_text, None
)
behavior.receive.return_value = mock_msg
@@ -119,7 +123,7 @@ async def test_run_message_from_transcriber_demo(behavior, mock_settings, monkey
assert (
sent_msg.to
== mock_settings.agent_settings.belief_collector_agent_name
== mock_settings.agent_settings.bdi_belief_collector_name
+ "@"
+ mock_settings.agent_settings.host
)
@@ -159,7 +163,7 @@ async def test_process_transcription_success(behavior, mock_settings):
# 2. Inspect the sent message
sent_msg: Message = behavior.send.call_args[0][0]
expected_to = (
mock_settings.agent_settings.belief_collector_agent_name
mock_settings.agent_settings.bdi_belief_collector_name
+ "@"
+ mock_settings.agent_settings.host
)

View File

@@ -1,101 +0,0 @@
import json
from unittest.mock import AsyncMock, MagicMock
import pytest
from control_backend.agents.belief_collector.behaviours.continuous_collect import (
ContinuousBeliefCollector,
)
def create_mock_message(sender_node: str, body: str) -> MagicMock:
"""Helper function to create a configured mock message."""
msg = MagicMock()
msg.sender.node = sender_node # MagicMock automatically creates nested mocks
msg.body = body
return msg
@pytest.fixture
def mock_agent(mocker):
"""Fixture to create a mock Agent."""
agent = MagicMock()
agent.jid = "belief_collector_agent@test"
return agent
@pytest.fixture
def continuous_collector(mock_agent, mocker):
"""Fixture to create an instance of ContinuousBeliefCollector with a mocked agent."""
# Patch asyncio.sleep to prevent tests from actually waiting
mocker.patch("asyncio.sleep", return_value=None)
collector = ContinuousBeliefCollector()
collector.agent = mock_agent
# Mock the receive method, we will control its return value in each test
collector.receive = AsyncMock()
return collector
@pytest.mark.asyncio
async def test_run_message_received(continuous_collector, mocker):
"""
Test that when a message is received, _process_message is called with that message.
"""
# Arrange
mock_msg = MagicMock()
continuous_collector.receive.return_value = mock_msg
mocker.patch.object(continuous_collector, "_process_message")
# Act
await continuous_collector.run()
# Assert
continuous_collector._process_message.assert_awaited_once_with(mock_msg)
@pytest.mark.asyncio
async def test_routes_to_handle_belief_text_by_type(continuous_collector, mocker):
msg = create_mock_message(
"anyone",
json.dumps({"type": "belief_extraction_text", "beliefs": {"user_said": [["hi"]]}}),
)
spy = mocker.patch.object(continuous_collector, "_handle_belief_text", new=AsyncMock())
await continuous_collector._process_message(msg)
spy.assert_awaited_once()
@pytest.mark.asyncio
async def test_routes_to_handle_belief_text_by_sender(continuous_collector, mocker):
msg = create_mock_message(
"belief_text_agent_mock", json.dumps({"beliefs": {"user_said": [["hi"]]}})
)
spy = mocker.patch.object(continuous_collector, "_handle_belief_text", new=AsyncMock())
await continuous_collector._process_message(msg)
spy.assert_awaited_once()
@pytest.mark.asyncio
async def test_routes_to_handle_emo_text(continuous_collector, mocker):
msg = create_mock_message("anyone", json.dumps({"type": "emotion_extraction_text"}))
spy = mocker.patch.object(continuous_collector, "_handle_emo_text", new=AsyncMock())
await continuous_collector._process_message(msg)
spy.assert_awaited_once()
@pytest.mark.asyncio
async def test_belief_text_happy_path_sends(continuous_collector, mocker):
payload = {"type": "belief_extraction_text", "beliefs": {"user_said": ["hello test", "No"]}}
continuous_collector.send = AsyncMock()
await continuous_collector._handle_belief_text(payload, "belief_text_agent_mock")
# make sure we attempted a send
continuous_collector.send.assert_awaited_once()
@pytest.mark.asyncio
async def test_belief_text_coerces_non_strings(continuous_collector, mocker):
payload = {"type": "belief_extraction_text", "beliefs": {"user_said": [["hi", 123]]}}
continuous_collector.send = AsyncMock()
await continuous_collector._handle_belief_text(payload, "origin")
continuous_collector.send.assert_awaited_once()

View File

@@ -1,7 +1,7 @@
import numpy as np
import pytest
from control_backend.agents.transcription.speech_recognizer import (
from control_backend.agents.perception.transcription_agent.speech_recognizer import (
OpenAIWhisperSpeechRecognizer,
SpeechRecognizer,
)
@@ -10,7 +10,7 @@ from control_backend.agents.transcription.speech_recognizer import (
@pytest.fixture(autouse=True)
def patch_sr_settings(monkeypatch):
# Patch the *module-local* settings that SpeechRecognizer imported
from control_backend.agents.transcription import speech_recognizer as sr
from control_backend.agents.perception.transcription_agent import speech_recognizer as sr
# Provide real numbers for everything _estimate_max_tokens() reads
monkeypatch.setattr(sr.settings.vad_settings, "sample_rate_hz", 16_000, raising=False)

View File

@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
import zmq
from control_backend.agents.vad_agent import SocketPoller
from control_backend.agents.perception.vad_agent import SocketPoller
@pytest.fixture
@@ -16,7 +16,7 @@ async def test_socket_poller_with_data(socket, mocker):
socket_data = b"test"
socket.recv.return_value = socket_data
mock_poller: MagicMock = mocker.patch("control_backend.agents.vad_agent.zmq.Poller")
mock_poller: MagicMock = mocker.patch("control_backend.agents.perception.vad_agent.zmq.Poller")
mock_poller.return_value.poll.return_value = [(socket, zmq.POLLIN)]
poller = SocketPoller(socket)
@@ -35,7 +35,7 @@ async def test_socket_poller_with_data(socket, mocker):
@pytest.mark.asyncio
async def test_socket_poller_no_data(socket, mocker):
mock_poller: MagicMock = mocker.patch("control_backend.agents.vad_agent.zmq.Poller")
mock_poller: MagicMock = mocker.patch("control_backend.agents.perception.vad_agent.zmq.Poller")
mock_poller.return_value.poll.return_value = []
poller = SocketPoller(socket)

View File

@@ -3,7 +3,7 @@ from unittest.mock import AsyncMock, MagicMock
import numpy as np
import pytest
from control_backend.agents.vad_agent import Streaming
from control_backend.agents.perception.vad_agent import StreamingBehaviour
@pytest.fixture
@@ -29,7 +29,7 @@ def streaming(audio_in_socket, audio_out_socket, mock_agent):
import torch
torch.hub.load.return_value = (..., ...) # Mock
streaming = Streaming(audio_in_socket, audio_out_socket)
streaming = StreamingBehaviour(audio_in_socket, audio_out_socket)
streaming._ready = True
streaming.agent = mock_agent
return streaming
@@ -38,7 +38,7 @@ def streaming(audio_in_socket, audio_out_socket, mock_agent):
@pytest.fixture(autouse=True)
def patch_settings(monkeypatch):
# Patch the settings that vad_agent.run() reads
from control_backend.agents import vad_agent
from control_backend.agents.perception import vad_agent
monkeypatch.setattr(
vad_agent.settings.behaviour_settings, "vad_prob_threshold", 0.5, raising=False