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)