Merge remote-tracking branch 'origin/dev' into feat/cb2ui-robot-connections
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -215,7 +215,8 @@ __marimo__/
|
|||||||
# Streamlit
|
# Streamlit
|
||||||
.streamlit/secrets.toml
|
.streamlit/secrets.toml
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
26
.gitlab-ci.yml
Normal file
26
.gitlab-ci.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# ---------- GLOBAL SETUP ---------- #
|
||||||
|
workflow:
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||||
|
|
||||||
|
stages:
|
||||||
|
- install
|
||||||
|
- lint
|
||||||
|
- test
|
||||||
|
|
||||||
|
variables:
|
||||||
|
UV_VERSION: "0.9.4"
|
||||||
|
PYTHON_VERSION: "3.13"
|
||||||
|
BASE_LAYER: trixie-slim
|
||||||
|
|
||||||
|
default:
|
||||||
|
image: ghcr.io/astral-sh/uv:$UV_VERSION-python$PYTHON_VERSION-$BASE_LAYER
|
||||||
|
|
||||||
|
# ---------- TESTING ---------- #
|
||||||
|
test:
|
||||||
|
stage: test
|
||||||
|
tags:
|
||||||
|
- test
|
||||||
|
script:
|
||||||
|
- uv run --only-group test pytest
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
We begin by installing UV (very nice utility for managing packages and Python version):
|
We begin by installing UV (very nice utility for managing packages and Python version):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# On macOS and Linux.
|
# On MacOS and Linux.
|
||||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
```
|
```
|
||||||
```bash
|
```bash
|
||||||
@@ -23,6 +23,13 @@ To run the project (development server), execute the following command (while in
|
|||||||
uv run fastapi dev src/control_backend/main.py
|
uv run fastapi dev src/control_backend/main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
Testing happens automatically when opening a merge request to any branch. If you want to manually run the test suite, you can do so by running the following:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run --only-group test pytest
|
||||||
|
```
|
||||||
|
|
||||||
## GitHooks
|
## GitHooks
|
||||||
|
|
||||||
To activate automatic commits/branch name checks run:
|
To activate automatic commits/branch name checks run:
|
||||||
|
|||||||
@@ -18,6 +18,18 @@ dependencies = [
|
|||||||
"pyzmq>=27.1.0",
|
"pyzmq>=27.1.0",
|
||||||
"silero-vad>=6.0.0",
|
"silero-vad>=6.0.0",
|
||||||
"spade>=4.1.0",
|
"spade>=4.1.0",
|
||||||
|
"spade-bdi>=0.3.2",
|
||||||
"torch>=2.8.0",
|
"torch>=2.8.0",
|
||||||
"uvicorn>=0.37.0",
|
"uvicorn>=0.37.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
test = [
|
||||||
|
"pytest>=8.4.2",
|
||||||
|
"pytest-asyncio>=1.2.0",
|
||||||
|
"pytest-cov>=7.0.0",
|
||||||
|
"pytest-mock>=3.15.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
pythonpath = ["src"]
|
||||||
|
|||||||
0
src/control_backend/agents/bdi/__init__.py
Normal file
0
src/control_backend/agents/bdi/__init__.py
Normal file
35
src/control_backend/agents/bdi/bdi_core.py
Normal file
35
src/control_backend/agents/bdi/bdi_core.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import agentspeak
|
||||||
|
from spade_bdi.bdi import BDIAgent
|
||||||
|
|
||||||
|
from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter
|
||||||
|
|
||||||
|
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()
|
||||||
|
self.add_behaviour(belief_setter)
|
||||||
|
|
||||||
|
def add_custom_actions(self, actions):
|
||||||
|
@actions.add(".reply", 1)
|
||||||
|
def _reply(agent, term, intention):
|
||||||
|
message = agentspeak.grounded(term.args[0], intention.scope)
|
||||||
|
self.logger.info(f"Replying to message: {message}")
|
||||||
|
reply = self._send_to_llm(message)
|
||||||
|
self.logger.info(f"Received reply: {reply}")
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
def _send_to_llm(self, message) -> str:
|
||||||
|
"""TODO: implement"""
|
||||||
|
return f"This is a reply to {message}"
|
||||||
|
|
||||||
|
|
||||||
60
src/control_backend/agents/bdi/behaviours/belief_setter.py
Normal file
60
src/control_backend/agents/bdi/behaviours/belief_setter.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from spade.agent import Message
|
||||||
|
from spade.behaviour import CyclicBehaviour
|
||||||
|
from spade_bdi.bdi import BDIAgent
|
||||||
|
|
||||||
|
from control_backend.core.config import settings
|
||||||
|
|
||||||
|
class BeliefSetter(CyclicBehaviour):
|
||||||
|
"""
|
||||||
|
This is the behaviour that the BDI agent runs.
|
||||||
|
This behaviour waits for incoming message and processes it based on sender.
|
||||||
|
Currently, t only waits for messages containing beliefs from Belief Collector and adds these to its KB.
|
||||||
|
"""
|
||||||
|
agent: BDIAgent
|
||||||
|
logger = logging.getLogger("BDI/Belief Setter")
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
msg = await self.receive(timeout=0.1)
|
||||||
|
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
|
||||||
|
self.logger.debug("Sender: %s", sender)
|
||||||
|
|
||||||
|
match sender:
|
||||||
|
case settings.agent_settings.belief_collector_agent_name:
|
||||||
|
self.logger.debug("Processing message from belief collector.")
|
||||||
|
self._process_belief_message(message)
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _process_belief_message(self, message: Message):
|
||||||
|
if not message.body: return
|
||||||
|
|
||||||
|
match message.thread:
|
||||||
|
case "beliefs":
|
||||||
|
try:
|
||||||
|
beliefs: dict[str, list[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]]]):
|
||||||
|
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)
|
||||||
3
src/control_backend/agents/bdi/rules.asl
Normal file
3
src/control_backend/agents/bdi/rules.asl
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
+user_said(Message) : not responded <-
|
||||||
|
+responded;
|
||||||
|
.reply(Message).
|
||||||
@@ -1,19 +1,25 @@
|
|||||||
|
# Standard library imports
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
# External imports
|
# External imports
|
||||||
import contextlib
|
import contextlib
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import logging
|
import logging
|
||||||
|
from spade.agent import Agent, Message
|
||||||
|
from spade.behaviour import OneShotBehaviour
|
||||||
import zmq
|
import zmq
|
||||||
|
|
||||||
# Internal imports
|
# Internal imports
|
||||||
from control_backend.agents.test_agent import TestAgent
|
|
||||||
from control_backend.agents.ri_communication_agent import RICommunicationAgent
|
from control_backend.agents.ri_communication_agent import RICommunicationAgent
|
||||||
|
from control_backend.agents.bdi.bdi_core import BDICoreAgent
|
||||||
from control_backend.api.v1.router import api_router
|
from control_backend.api.v1.router import api_router
|
||||||
from control_backend.core.config import settings
|
from control_backend.core.config import AgentSettings, settings
|
||||||
from control_backend.core.zmq_context import context
|
from control_backend.core.zmq_context import context
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
@@ -32,10 +38,12 @@ async def lifespan(app: FastAPI):
|
|||||||
address="tcp://*:5555", bind=True)
|
address="tcp://*:5555", bind=True)
|
||||||
await ri_communication_agent.start()
|
await ri_communication_agent.start()
|
||||||
|
|
||||||
|
bdi_core = BDICoreAgent(settings.agent_settings.bdi_core_agent_name + '@' + settings.agent_settings.host, settings.agent_settings.bdi_core_agent_name, "src/control_backend/agents/bdi/rules.asl")
|
||||||
|
await bdi_core.start()
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
logger.info("%s shutting down.", app.title)
|
logger.info("%s shutting down.", app.title)
|
||||||
|
|
||||||
|
|
||||||
# if __name__ == "__main__":
|
# if __name__ == "__main__":
|
||||||
app = FastAPI(title=settings.app_title, lifespan=lifespan)
|
app = FastAPI(title=settings.app_title, lifespan=lifespan)
|
||||||
|
|||||||
37
test/conftest.py
Normal file
37
test/conftest.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import sys
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
"""
|
||||||
|
This hook runs at the start of the pytest session, before any tests are
|
||||||
|
collected. It mocks heavy or unavailable modules to prevent ImportErrors.
|
||||||
|
"""
|
||||||
|
# --- Mock spade and spade-bdi ---
|
||||||
|
mock_spade = MagicMock()
|
||||||
|
mock_spade.agent = MagicMock()
|
||||||
|
mock_spade.behaviour = MagicMock()
|
||||||
|
mock_spade_bdi = MagicMock()
|
||||||
|
mock_spade_bdi.bdi = MagicMock()
|
||||||
|
|
||||||
|
mock_spade.agent.Message = MagicMock()
|
||||||
|
mock_spade.behaviour.CyclicBehaviour = type('CyclicBehaviour', (object,), {})
|
||||||
|
mock_spade_bdi.bdi.BDIAgent = type('BDIAgent', (object,), {})
|
||||||
|
|
||||||
|
sys.modules['spade'] = mock_spade
|
||||||
|
sys.modules['spade.agent'] = mock_spade.agent
|
||||||
|
sys.modules['spade.behaviour'] = mock_spade.behaviour
|
||||||
|
sys.modules['spade_bdi'] = mock_spade_bdi
|
||||||
|
sys.modules['spade_bdi.bdi'] = mock_spade_bdi.bdi
|
||||||
|
|
||||||
|
# --- Mock the config module to prevent Pydantic ImportError ---
|
||||||
|
mock_config_module = MagicMock()
|
||||||
|
|
||||||
|
# The code under test does `from ... import settings`, so our mock module
|
||||||
|
# must have a `settings` attribute. We'll make it a MagicMock so we can
|
||||||
|
# configure it later in our tests using mocker.patch.
|
||||||
|
mock_config_module.settings = MagicMock()
|
||||||
|
|
||||||
|
sys.modules['control_backend.core.config'] = mock_config_module
|
||||||
236
test/unit/agents/bdi/behaviours/test_belief_setter.py
Normal file
236
test/unit/agents/bdi/behaviours/test_belief_setter.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from unittest.mock import MagicMock, AsyncMock, call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter
|
||||||
|
|
||||||
|
# Define a constant for the collector agent name to use in tests
|
||||||
|
COLLECTOR_AGENT_NAME = "belief_collector"
|
||||||
|
COLLECTOR_AGENT_JID = f"{COLLECTOR_AGENT_NAME}@test"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_agent(mocker):
|
||||||
|
"""Fixture to create a mock BDIAgent."""
|
||||||
|
agent = MagicMock()
|
||||||
|
agent.bdi = MagicMock()
|
||||||
|
agent.jid = "bdi_agent@test"
|
||||||
|
return agent
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def belief_setter(mock_agent, mocker):
|
||||||
|
"""Fixture to create an instance of BeliefSetter 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.agent = mock_agent
|
||||||
|
# Mock the receive method, we will control its return value in each test
|
||||||
|
setter.receive = AsyncMock()
|
||||||
|
return setter
|
||||||
|
|
||||||
|
|
||||||
|
def create_mock_message(sender_node: str, body: str, thread: 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
|
||||||
|
msg.thread = thread
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_no_message_received(belief_setter, mocker):
|
||||||
|
"""
|
||||||
|
Test that when no message is received, _process_message is not called.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
belief_setter.receive.return_value = None
|
||||||
|
mocker.patch.object(belief_setter, "_process_message")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
await belief_setter.run()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
belief_setter._process_message.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_run_message_received(belief_setter, 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")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
await belief_setter.run()
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
belief_setter._process_message.assert_called_once_with(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_message_from_belief_collector(belief_setter, 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")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
belief_setter._process_message(msg)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_process_belief.assert_called_once_with(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_message_from_other_agent(belief_setter, 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")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
belief_setter._process_message(msg)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_process_belief.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
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"]]
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
belief_setter._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):
|
||||||
|
"""
|
||||||
|
Test that a message with invalid JSON is handled gracefully and an error is logged.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
with caplog.at_level(logging.ERROR):
|
||||||
|
belief_setter._process_belief_message(msg)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_set_beliefs.assert_not_called()
|
||||||
|
assert "Could not decode beliefs into JSON format" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_belief_message_wrong_thread(belief_setter, mocker):
|
||||||
|
"""
|
||||||
|
Test that a message with an incorrect thread is ignored.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
belief_setter._process_belief_message(msg)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_set_beliefs.assert_not_called()
|
||||||
|
|
||||||
|
def test_process_belief_message_empty_body(belief_setter, 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")
|
||||||
|
|
||||||
|
# Act
|
||||||
|
belief_setter._process_belief_message(msg)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
mock_set_beliefs.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_beliefs_success(belief_setter, mock_agent, caplog):
|
||||||
|
"""
|
||||||
|
Test that beliefs are correctly set on the agent's BDI.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
beliefs_to_set = {
|
||||||
|
"is_hot": [["kitchen"], ["living_room"]],
|
||||||
|
"door_is": [["front_door", "closed"]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Act
|
||||||
|
with caplog.at_level(logging.INFO):
|
||||||
|
belief_setter._set_beliefs(beliefs_to_set)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
expected_calls = [
|
||||||
|
call("is_hot", "kitchen"),
|
||||||
|
call("is_hot", "living_room"),
|
||||||
|
call("door_is", "front_door", "closed")
|
||||||
|
]
|
||||||
|
mock_agent.bdi.set_belief.assert_has_calls(expected_calls, any_order=True)
|
||||||
|
assert mock_agent.bdi.set_belief.call_count == 3
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_beliefs_bdi_not_initialized(belief_setter, mock_agent, caplog):
|
||||||
|
"""
|
||||||
|
Test that a warning is logged if the agent's BDI is not initialized.
|
||||||
|
"""
|
||||||
|
# Arrange
|
||||||
|
mock_agent.bdi = None # Simulate BDI not being ready
|
||||||
|
beliefs_to_set = {"is_hot": [["kitchen"]]}
|
||||||
|
|
||||||
|
# Act
|
||||||
|
with caplog.at_level(logging.WARNING):
|
||||||
|
belief_setter._set_beliefs(beliefs_to_set)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert "Cannot set beliefs, since agent's BDI is not yet initialized." in caplog.text
|
||||||
59
uv.lock
generated
59
uv.lock
generated
@@ -6,6 +6,18 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.14'",
|
"python_full_version < '3.14'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "agentspeak"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/a3/f8e9292cfd47aa5558f4578c498ca12c068a3a1d60ddfd0af13a87c1e47a/agentspeak-0.2.2.tar.gz", hash = "sha256:7c7fcf689fd54460597be1798ce11535f42a60c3d79af59381af3e13ef7a41bb", size = 59628, upload-time = "2024-03-21T11:55:39.026Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/b5/e95cbd9d9e999ac8dc4e0bb7a940112a2751cf98880b4ff0626e53d14249/agentspeak-0.2.2-py3-none-any.whl", hash = "sha256:9b454bc0adf63cb0d73fb4a3a9a489e7d892d5fbf17f750de532670736c0c4dd", size = 61628, upload-time = "2024-03-21T11:55:36.741Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiodns"
|
name = "aiodns"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
@@ -700,11 +712,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.1.0"
|
version = "2.3.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1295,10 +1307,19 @@ dependencies = [
|
|||||||
{ name = "pyzmq" },
|
{ name = "pyzmq" },
|
||||||
{ name = "silero-vad" },
|
{ name = "silero-vad" },
|
||||||
{ name = "spade" },
|
{ name = "spade" },
|
||||||
|
{ name = "spade-bdi" },
|
||||||
{ name = "torch" },
|
{ name = "torch" },
|
||||||
{ name = "uvicorn" },
|
{ name = "uvicorn" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
test = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
|
{ name = "pytest-mock" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "fastapi", extras = ["all"], specifier = ">=0.115.6" },
|
{ name = "fastapi", extras = ["all"], specifier = ">=0.115.6" },
|
||||||
@@ -1314,10 +1335,28 @@ requires-dist = [
|
|||||||
{ name = "pyzmq", specifier = ">=27.1.0" },
|
{ name = "pyzmq", specifier = ">=27.1.0" },
|
||||||
{ name = "silero-vad", specifier = ">=6.0.0" },
|
{ name = "silero-vad", specifier = ">=6.0.0" },
|
||||||
{ name = "spade", specifier = ">=4.1.0" },
|
{ name = "spade", specifier = ">=4.1.0" },
|
||||||
|
{ name = "spade-bdi", specifier = ">=0.3.2" },
|
||||||
{ name = "torch", specifier = ">=2.8.0" },
|
{ name = "torch", specifier = ">=2.8.0" },
|
||||||
{ name = "uvicorn", specifier = ">=0.37.0" },
|
{ name = "uvicorn", specifier = ">=0.37.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
test = [
|
||||||
|
{ name = "pytest", specifier = ">=8.4.2" },
|
||||||
|
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
|
||||||
|
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
||||||
|
{ name = "pytest-mock", specifier = ">=3.15.1" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -2082,6 +2121,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e9/06/21d0e937f4daa905a9a007700f59b06de644a44e5f594c3428c3ff93ca39/spade-4.1.0-py2.py3-none-any.whl", hash = "sha256:8b20e7fcb12f836cb0504e9da31f7bd867c7276440e19ebca864aecabc71b114", size = 37033, upload-time = "2025-05-22T17:19:06.524Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/06/21d0e937f4daa905a9a007700f59b06de644a44e5f594c3428c3ff93ca39/spade-4.1.0-py2.py3-none-any.whl", hash = "sha256:8b20e7fcb12f836cb0504e9da31f7bd867c7276440e19ebca864aecabc71b114", size = 37033, upload-time = "2025-05-22T17:19:06.524Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spade-bdi"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "agentspeak" },
|
||||||
|
{ name = "loguru" },
|
||||||
|
{ name = "spade" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/16/b4/d52d9d06ad17d4b3a90ca11b64a14194f3f944f561f4da1395ce3fe3994d/spade_bdi-0.3.2.tar.gz", hash = "sha256:5d03661425f78771e39f3592f8a602ff8240465682b79d333926d3e562657d81", size = 21208, upload-time = "2025-01-03T14:16:43.755Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/c2/986de9abaad805d92a33912ab06b08bb81bd404bcef9ad0f2fd7a09f274b/spade_bdi-0.3.2-py2.py3-none-any.whl", hash = "sha256:2039271f586b108660a0a6a951d9ec815197caf14915317c6eec19ff496c2cff", size = 7416, upload-time = "2025-01-03T14:16:42.226Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "2.0.40"
|
version = "2.0.40"
|
||||||
|
|||||||
Reference in New Issue
Block a user