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
This commit is contained in:
2025-10-18 17:50:17 +02:00
parent 8812c5f5f9
commit 31882f8d63
9 changed files with 153 additions and 7 deletions

View File

@@ -14,6 +14,7 @@ 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",
] ]

View File

@@ -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}"

View File

@@ -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)

View File

@@ -0,0 +1,3 @@
+user_said(Message) : not responded <-
+responded;
.reply(Message).

View File

@@ -1,15 +1,24 @@
from re import L
from pydantic import BaseModel from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
class ZMQSettings(BaseModel): class ZMQSettings(BaseModel):
internal_comm_address: str = "tcp://localhost:5560" 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): class Settings(BaseSettings):
app_title: str = "PepperPlus" app_title: str = "PepperPlus"
ui_url: str = "http://localhost:5173" ui_url: str = "http://localhost:5173"
zmq_settings: ZMQSettings = ZMQSettings() zmq_settings: ZMQSettings = ZMQSettings()
agent_settings: AgentSettings = AgentSettings()
model_config = SettingsConfigDict(env_file=".env") model_config = SettingsConfigDict(env_file=".env")

View File

@@ -1,18 +1,24 @@
# 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.bdi.bdi_core import BDICore
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):
@@ -26,13 +32,23 @@ async def lifespan(app: FastAPI):
logger.info("Internal publishing socket bound to %s", internal_comm_socket) logger.info("Internal publishing socket bound to %s", internal_comm_socket)
# Initiate agents # Initiate agents
test_agent = TestAgent("test_agent@localhost", "test_agent") 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 test_agent.start() 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 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)

28
uv.lock generated
View File

@@ -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"
@@ -1221,6 +1233,7 @@ 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" },
] ]
@@ -1236,6 +1249,7 @@ 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" },
] ]
@@ -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" }, { 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"