From 4d076eac4838d8a972430207cfb2780ed59d2cbc Mon Sep 17 00:00:00 2001 From: Kasper Date: Sat, 22 Nov 2025 19:53:19 +0100 Subject: [PATCH] perf: improved speed of BDI By efficiently checking when the next work has to be done, we can increase performance not having to "busy loop". Time from transcription -> message to LLM agent is now down to sub 1 millisecond. ref: N25B-316 --- src/control_backend/agents/bdi/__init__.py | 4 +-- .../bdi/bdi_core_agent/bdi_core_agent.py | 33 +++++++++++++++++-- .../belief_collector_agent.py | 0 .../text_belief_extractor_agent.py | 0 .../agents/perception/vad_agent.py | 9 ++--- 5 files changed, 35 insertions(+), 11 deletions(-) rename src/control_backend/agents/bdi/{belief_collector_agent => }/belief_collector_agent.py (100%) rename src/control_backend/agents/bdi/{text_belief_extractor_agent => }/text_belief_extractor_agent.py (100%) diff --git a/src/control_backend/agents/bdi/__init__.py b/src/control_backend/agents/bdi/__init__.py index a7f6082..c8c8d47 100644 --- a/src/control_backend/agents/bdi/__init__.py +++ b/src/control_backend/agents/bdi/__init__.py @@ -1,7 +1,7 @@ from .bdi_core_agent.bdi_core_agent import BDICoreAgent as BDICoreAgent -from .belief_collector_agent.belief_collector_agent import ( +from .belief_collector_agent import ( BDIBeliefCollectorAgent as BDIBeliefCollectorAgent, ) -from .text_belief_extractor_agent.text_belief_extractor_agent import ( +from .text_belief_extractor_agent import ( TextBeliefExtractorAgent as TextBeliefExtractorAgent, ) diff --git a/src/control_backend/agents/bdi/bdi_core_agent/bdi_core_agent.py b/src/control_backend/agents/bdi/bdi_core_agent/bdi_core_agent.py index 6421383..0a64ee7 100644 --- a/src/control_backend/agents/bdi/bdi_core_agent/bdi_core_agent.py +++ b/src/control_backend/agents/bdi/bdi_core_agent/bdi_core_agent.py @@ -1,6 +1,7 @@ import asyncio import copy import json +import time from collections.abc import Iterable import agentspeak @@ -22,6 +23,7 @@ class BDICoreAgent(BaseAgent): self.env = agentspeak.runtime.Environment() # Deep copy because we don't actually want to modify the standard actions globally self.actions = copy.deepcopy(agentspeak.stdlib.actions) + self._wake_bdi_loop = asyncio.Event() async def setup(self) -> None: self.logger.debug("Setup started.") @@ -32,6 +34,7 @@ class BDICoreAgent(BaseAgent): # Start the BDI cycle loop await self.add_behavior(self._bdi_loop()) + self._wake_bdi_loop.set() self.logger.debug("Setup complete.") async def _load_asl(self): @@ -43,10 +46,28 @@ class BDICoreAgent(BaseAgent): self.bdi_agent = agentspeak.runtime.Agent(self.env, self.name) async def _bdi_loop(self): - """Runs the AgentSpeak BDI loop.""" + """ + Runs the AgentSpeak BDI loop. Efficiently checks for when the next expected work will be. + """ while self._running: - self.bdi_agent.step() - await asyncio.sleep(0.01) + await ( + self._wake_bdi_loop.wait() + ) # gets set whenever there's an update to the belief base + + # Agent knows when it's expected to have to do its next thing + maybe_more_work = True + while maybe_more_work: + maybe_more_work = False + if self.bdi_agent.step(): + maybe_more_work = True + + if not maybe_more_work: + deadline = self.bdi_agent.shortest_deadline() + if deadline: + await asyncio.sleep(deadline - time.time()) + maybe_more_work = True + else: + self._wake_bdi_loop.clear() async def handle_message(self, msg: InternalMessage): """ @@ -93,6 +114,9 @@ class BDICoreAgent(BaseAgent): term, agentspeak.runtime.Intention(), ) + + self._wake_bdi_loop.set() + self.logger.debug(f"Added belief {self.format_belief_string(name, args)}") def _remove_belief(self, name: str, args: Iterable[str]): @@ -111,6 +135,7 @@ class BDICoreAgent(BaseAgent): if result: self.logger.debug(f"Removed belief {self.format_belief_string(name, args)}") + self._wake_bdi_loop.set() else: self.logger.debug("Failed to remove belief (it was not in the belief base).") @@ -135,6 +160,8 @@ class BDICoreAgent(BaseAgent): ) removed_count += 1 + self._wake_bdi_loop.set() + self.logger.debug(f"Removed {removed_count} beliefs.") def _add_custom_actions(self) -> None: diff --git a/src/control_backend/agents/bdi/belief_collector_agent/belief_collector_agent.py b/src/control_backend/agents/bdi/belief_collector_agent.py similarity index 100% rename from src/control_backend/agents/bdi/belief_collector_agent/belief_collector_agent.py rename to src/control_backend/agents/bdi/belief_collector_agent.py diff --git a/src/control_backend/agents/bdi/text_belief_extractor_agent/text_belief_extractor_agent.py b/src/control_backend/agents/bdi/text_belief_extractor_agent.py similarity index 100% rename from src/control_backend/agents/bdi/text_belief_extractor_agent/text_belief_extractor_agent.py rename to src/control_backend/agents/bdi/text_belief_extractor_agent.py diff --git a/src/control_backend/agents/perception/vad_agent.py b/src/control_backend/agents/perception/vad_agent.py index b257137..ab6d6c7 100644 --- a/src/control_backend/agents/perception/vad_agent.py +++ b/src/control_backend/agents/perception/vad_agent.py @@ -63,7 +63,7 @@ class VADAgent(BaseAgent): self.audio_buffer = np.array([], dtype=np.float32) self.i_since_speech = settings.behaviour_settings.vad_initial_since_speech - self._ready = False + self._ready = asyncio.Event() self.model = None async def setup(self): @@ -141,14 +141,11 @@ class VADAgent(BaseAgent): while await self.audio_in_poller.poll(1) is not None: discarded += 1 self.logger.info(f"Discarded {discarded} audio packets before starting.") - self._ready = True + self._ready.set() async def _streaming_loop(self): + await self._ready.wait() while self._running: - if not self._ready: - await asyncio.sleep(0.1) - continue - assert self.audio_in_poller is not None data = await self.audio_in_poller.poll() if data is None: