From 31882f8d63140515f011a427ad8cc178138abab8 Mon Sep 17 00:00:00 2001 From: Kasper Date: Sat, 18 Oct 2025 17:50:17 +0200 Subject: [PATCH] feat: add BDI core agent Main BDI brain structure implemented. Still some TODOs left, and very basic implementation (only one belief "user_said(Message)" and every message is sent straight to a function which is responsible for getting an LLM response. ref: N25B-197 --- pyproject.toml | 1 + src/control_backend/agents/bdi/__init__.py | 0 src/control_backend/agents/bdi/bdi_core.py | 32 +++++++++++ .../agents/bdi/behaviours/__init__.py | 0 .../agents/bdi/behaviours/belief_setter.py | 57 +++++++++++++++++++ src/control_backend/agents/bdi/rules.asl | 3 + src/control_backend/core/config.py | 11 +++- src/control_backend/main.py | 28 +++++++-- uv.lock | 28 +++++++++ 9 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 src/control_backend/agents/bdi/__init__.py create mode 100644 src/control_backend/agents/bdi/bdi_core.py create mode 100644 src/control_backend/agents/bdi/behaviours/__init__.py create mode 100644 src/control_backend/agents/bdi/behaviours/belief_setter.py create mode 100644 src/control_backend/agents/bdi/rules.asl diff --git a/pyproject.toml b/pyproject.toml index d0a617f..e2f3b8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "pyzmq>=27.1.0", "silero-vad>=6.0.0", "spade>=4.1.0", + "spade-bdi>=0.3.2", "torch>=2.8.0", "uvicorn>=0.37.0", ] diff --git a/src/control_backend/agents/bdi/__init__.py b/src/control_backend/agents/bdi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/control_backend/agents/bdi/bdi_core.py b/src/control_backend/agents/bdi/bdi_core.py new file mode 100644 index 0000000..d0c8b6c --- /dev/null +++ b/src/control_backend/agents/bdi/bdi_core.py @@ -0,0 +1,32 @@ +import logging + +import agentspeak +from spade_bdi.bdi import BDIAgent + +from control_backend.agents.bdi.behaviours.belief_setter import BeliefSetter + +class BDICore(BDIAgent): + """ + TODO: docs + """ + 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}" + + diff --git a/src/control_backend/agents/bdi/behaviours/__init__.py b/src/control_backend/agents/bdi/behaviours/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/control_backend/agents/bdi/behaviours/belief_setter.py b/src/control_backend/agents/bdi/behaviours/belief_setter.py new file mode 100644 index 0000000..c8d4c2e --- /dev/null +++ b/src/control_backend/agents/bdi/behaviours/belief_setter.py @@ -0,0 +1,57 @@ +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): + """ + TODO: docs + """ + 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) diff --git a/src/control_backend/agents/bdi/rules.asl b/src/control_backend/agents/bdi/rules.asl new file mode 100644 index 0000000..41660a4 --- /dev/null +++ b/src/control_backend/agents/bdi/rules.asl @@ -0,0 +1,3 @@ ++user_said(Message) : not responded <- + +responded; + .reply(Message). diff --git a/src/control_backend/core/config.py b/src/control_backend/core/config.py index fca21b3..07a828d 100644 --- a/src/control_backend/core/config.py +++ b/src/control_backend/core/config.py @@ -1,15 +1,24 @@ +from re import L from pydantic import BaseModel from pydantic_settings import BaseSettings, SettingsConfigDict class ZMQSettings(BaseModel): internal_comm_address: str = "tcp://localhost:5560" +class AgentSettings(BaseModel): + host: str = "localhost" + bdi_core_agent_name: str = "bdi_core" + belief_collector_agent_name: str = "belief_collector" + test_agent_name: str = "test_agent" + class Settings(BaseSettings): app_title: str = "PepperPlus" - + ui_url: str = "http://localhost:5173" zmq_settings: ZMQSettings = ZMQSettings() + + agent_settings: AgentSettings = AgentSettings() model_config = SettingsConfigDict(env_file=".env") diff --git a/src/control_backend/main.py b/src/control_backend/main.py index cd4d3fa..5ec0276 100644 --- a/src/control_backend/main.py +++ b/src/control_backend/main.py @@ -1,18 +1,24 @@ +# Standard library imports +import asyncio +import json + # External imports import contextlib from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import logging +from spade.agent import Agent, Message +from spade.behaviour import OneShotBehaviour import zmq # Internal imports -from control_backend.agents.test_agent import TestAgent +from control_backend.agents.bdi.bdi_core import BDICore 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 logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) +logging.basicConfig(level=logging.DEBUG) @contextlib.asynccontextmanager async def lifespan(app: FastAPI): @@ -26,13 +32,23 @@ async def lifespan(app: FastAPI): logger.info("Internal publishing socket bound to %s", internal_comm_socket) # Initiate agents - test_agent = TestAgent("test_agent@localhost", "test_agent") - await test_agent.start() + bdi_core = BDICore(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() + # -----------TEMORARY SECTION------------- + belief_collector = Agent(settings.agent_settings.belief_collector_agent_name + '@' + settings.agent_settings.host, settings.agent_settings.belief_collector_agent_name) + await belief_collector.start() + + class SendMessageBehaviour(OneShotBehaviour): + async def run(self): + await self.send(Message(bdi_core.jid, belief_collector.jid, json.dumps({"user_said": [["Hello World!"]]}), "beliefs")) + + belief_collector.add_behaviour(SendMessageBehaviour()) + # -----------TEMORARY SECTION------------- + yield logger.info("%s shutting down.", app.title) - # if __name__ == "__main__": app = FastAPI(title=settings.app_title, lifespan=lifespan) diff --git a/uv.lock b/uv.lock index 07bdb8f..bddde4d 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,18 @@ resolution-markers = [ "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]] name = "aiodns" version = "3.5.0" @@ -1221,6 +1233,7 @@ dependencies = [ { name = "pyzmq" }, { name = "silero-vad" }, { name = "spade" }, + { name = "spade-bdi" }, { name = "torch" }, { name = "uvicorn" }, ] @@ -1236,6 +1249,7 @@ requires-dist = [ { name = "pyzmq", specifier = ">=27.1.0" }, { name = "silero-vad", specifier = ">=6.0.0" }, { name = "spade", specifier = ">=4.1.0" }, + { name = "spade-bdi", specifier = ">=0.3.2" }, { name = "torch", specifier = ">=2.8.0" }, { name = "uvicorn", specifier = ">=0.37.0" }, ] @@ -1941,6 +1955,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" }, ] +[[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]] name = "sqlalchemy" version = "2.0.40"