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:
@@ -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",
|
||||
]
|
||||
|
||||
0
src/control_backend/agents/bdi/__init__.py
Normal file
0
src/control_backend/agents/bdi/__init__.py
Normal file
32
src/control_backend/agents/bdi/bdi_core.py
Normal file
32
src/control_backend/agents/bdi/bdi_core.py
Normal 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}"
|
||||
|
||||
|
||||
57
src/control_backend/agents/bdi/behaviours/belief_setter.py
Normal file
57
src/control_backend/agents/bdi/behaviours/belief_setter.py
Normal 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)
|
||||
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,9 +1,16 @@
|
||||
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"
|
||||
|
||||
@@ -11,6 +18,8 @@ class Settings(BaseSettings):
|
||||
|
||||
zmq_settings: ZMQSettings = ZMQSettings()
|
||||
|
||||
agent_settings: AgentSettings = AgentSettings()
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -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,14 +32,24 @@ 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)
|
||||
|
||||
|
||||
28
uv.lock
generated
28
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user