Source code for simvx.core.ai.brain
"""The `perceive -> decide -> act` contract shared by classical and LLM AI.
A `Brain` perceives (sensors write the blackboard), decides (returns an
`Action` or ``None``), and acts (executes the action and feeds the result
back onto the blackboard). Classical brains decide every frame from a cheap
behaviour tree / state machine / utility scorer; an LLM brain decides
asynchronously and infrequently, writing high-level intent the classical
layer executes. They are interchangeable because they share this contract and
a `Blackboard`.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any
from .blackboard import Blackboard
from .sensor import Perception
#: Blackboard key under which `Brain.act` stores the most recent result, so a
#: brain's next decision can see what actually happened last tick.
LAST_RESULT_KEY = "last_action_result"
[docs]
@dataclass
class ActionResult:
"""Outcome of an action, fed back so the next decision stays consistent."""
ok: bool
message: str = ""
data: Any = None
[docs]
@classmethod
def success(cls, message: str = "", data: Any = None) -> ActionResult:
return cls(True, message, data)
[docs]
@classmethod
def failure(cls, message: str = "", data: Any = None) -> ActionResult:
return cls(False, message, data)
[docs]
def __bool__(self) -> bool:
return self.ok
[docs]
@dataclass
class AIContext:
"""Everything a brain needs to perceive, decide, and act this tick."""
agent: Any = None
blackboard: Blackboard = field(default_factory=Blackboard)
dt: float = 0.0
now: float = 0.0
world: Any = None
[docs]
class Action(ABC):
"""A unit of behaviour the engine executes deterministically."""
[docs]
@abstractmethod
def execute(self, ctx: AIContext) -> ActionResult: ...
[docs]
class Brain(ABC):
"""Base class for any decision-maker driving an agent."""
#: Optional sensors run before each decision; subclasses may set their own.
perception: Perception | None = None
[docs]
def perceive(self, ctx: AIContext) -> None:
if self.perception is not None:
self.perception.sense(ctx)
[docs]
@abstractmethod
def decide(self, ctx: AIContext) -> Action | None:
"""Return the action to take, or ``None`` to do nothing this tick."""
[docs]
def act(self, action: Action, ctx: AIContext) -> ActionResult:
result = action.execute(ctx)
ctx.blackboard.set(LAST_RESULT_KEY, result)
return result
[docs]
def tick(self, ctx: AIContext) -> ActionResult | None:
"""Run one full perceive -> decide -> act cycle."""
self.perceive(ctx)
action = self.decide(ctx)
if action is None:
return None
return self.act(action, ctx)