Spaces:
Runtime error
Runtime error
| """ | |
| core.behavior | |
| ============= | |
| Base behavior abstraction for WorldSmithAI. | |
| This module defines the generic Behavior interface used by all concrete | |
| simulation behaviors. Behaviors are domain-agnostic executable actions attached | |
| to agents. They do not assume any specific world type, species, economy, | |
| civilization, or ecosystem. | |
| Examples of concrete behaviors that can inherit from Behavior include: | |
| - MoveBehavior | |
| - WanderBehavior | |
| - SeekBehavior | |
| - ConsumeBehavior | |
| - TradeBehavior | |
| - AttackBehavior | |
| - ResearchBehavior | |
| - CollaborateBehavior | |
| The Agent class delegates action execution to Behavior objects. Policies decide | |
| which behavior an agent should attempt, while behaviors decide how that action | |
| affects the agent and world. | |
| Minimal usage example | |
| --------------------- | |
| from core.agent import Agent | |
| from core.behavior import Behavior, BehaviorExecution | |
| class RestBehavior(Behavior): | |
| def execute(self, agent, world): | |
| energy = float(agent.state.get("energy", 0.0)) | |
| return self.success( | |
| reward=0.1, | |
| state_updates={"energy": min(1.0, energy + 0.1)}, | |
| message="Agent rested.", | |
| ) | |
| agent = Agent( | |
| id="agent_1", | |
| type="researcher", | |
| position=[0.0, 0.0], | |
| state={"energy": 0.5}, | |
| behaviors={"rest": RestBehavior(id="rest")}, | |
| ) | |
| result = agent.step(world) | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from abc import ABC, abstractmethod | |
| from copy import deepcopy | |
| from dataclasses import dataclass, field | |
| from typing import Any, Final, Mapping, Sequence, TypeAlias, TypeVar, cast | |
| import numpy as np | |
| from numpy.typing import NDArray | |
| from core.agent import Agent, BehaviorExecution, WorldProtocol | |
| logger = logging.getLogger(__name__) | |
| BehaviorResult: TypeAlias = BehaviorExecution | Mapping[str, Any] | None | |
| PositionLike: TypeAlias = Sequence[float] | NDArray[np.float64] | |
| T = TypeVar("T") | |
| _MISSING: Final[object] = object() | |
| class Behavior(ABC): | |
| """ | |
| Abstract base class for all agent behaviors. | |
| A behavior is a reusable action unit that can be attached to any agent. | |
| Behaviors should be domain-agnostic where possible and configured through | |
| parameters supplied by the DSL. | |
| Attributes | |
| ---------- | |
| id: | |
| Unique behavior identifier within an agent's behavior map. | |
| name: | |
| Optional human-readable behavior name. Defaults to ``id``. | |
| parameters: | |
| Generic configuration dictionary, usually populated by the DSL. | |
| metadata: | |
| Optional structured metadata describing the behavior. | |
| enabled: | |
| Whether this behavior is currently available for execution. | |
| Notes | |
| ----- | |
| Concrete subclasses must implement ``execute()``. | |
| Subclasses may optionally override ``_check_preconditions()`` for custom | |
| precondition logic. They should generally avoid overriding | |
| ``check_preconditions()`` directly unless they need to replace the full | |
| precondition pipeline. | |
| """ | |
| id: str | |
| name: str | None = None | |
| parameters: dict[str, Any] = field(default_factory=dict) | |
| metadata: dict[str, Any] = field(default_factory=dict) | |
| enabled: bool = True | |
| def __post_init__(self) -> None: | |
| """ | |
| Validate and normalize behavior fields. | |
| Raises | |
| ------ | |
| ValueError | |
| If ``id`` or ``name`` is invalid. | |
| TypeError | |
| If ``parameters``, ``metadata``, or ``enabled`` has an invalid type. | |
| """ | |
| if not isinstance(self.id, str) or not self.id.strip(): | |
| raise ValueError("Behavior.id must be a non-empty string.") | |
| self.id = self.id.strip() | |
| if self.name is None: | |
| self.name = self.id | |
| elif not isinstance(self.name, str) or not self.name.strip(): | |
| raise ValueError("Behavior.name must be None or a non-empty string.") | |
| else: | |
| self.name = self.name.strip() | |
| if not isinstance(self.parameters, Mapping): | |
| raise TypeError("Behavior.parameters must be a mapping.") | |
| if not isinstance(self.metadata, Mapping): | |
| raise TypeError("Behavior.metadata must be a mapping.") | |
| self.parameters = dict(self.parameters) | |
| self.metadata = dict(self.metadata) | |
| self._validate_string_keys(self.parameters, "parameters") | |
| self._validate_string_keys(self.metadata, "metadata") | |
| if not isinstance(self.enabled, bool): | |
| raise TypeError("Behavior.enabled must be a boolean.") | |
| def check_preconditions(self, agent: Agent, world: WorldProtocol) -> bool: | |
| """ | |
| Return whether this behavior can currently execute. | |
| This method applies shared lifecycle checks before delegating to | |
| subclass-specific preconditions. | |
| Parameters | |
| ---------- | |
| agent: | |
| Agent attempting to execute this behavior. | |
| world: | |
| World in which execution would occur. | |
| Returns | |
| ------- | |
| bool | |
| True if the behavior can execute, otherwise False. | |
| """ | |
| if not self.enabled: | |
| return False | |
| if not agent.alive: | |
| return False | |
| return self._check_preconditions(agent, world) | |
| def _check_preconditions(self, agent: Agent, world: WorldProtocol) -> bool: | |
| """ | |
| Return subclass-specific precondition status. | |
| Subclasses can override this method to enforce requirements such as | |
| minimum energy, nearby resources, available trade partners, research | |
| prerequisites, or attack range. | |
| Parameters | |
| ---------- | |
| agent: | |
| Agent attempting to execute this behavior. | |
| world: | |
| World in which execution would occur. | |
| Returns | |
| ------- | |
| bool | |
| True if subclass-specific conditions are satisfied. | |
| """ | |
| return True | |
| def execute(self, agent: Agent, world: WorldProtocol) -> BehaviorResult: | |
| """ | |
| Execute this behavior. | |
| Concrete subclasses must implement this method. Implementations may | |
| update world objects directly through world APIs, and should return a | |
| ``BehaviorExecution`` or compatible mapping describing the agent-level | |
| result. | |
| Parameters | |
| ---------- | |
| agent: | |
| Agent executing the behavior. | |
| world: | |
| World in which the behavior executes. | |
| Returns | |
| ------- | |
| BehaviorResult | |
| Normalized execution result or mapping compatible with | |
| ``BehaviorExecution``. | |
| """ | |
| raise NotImplementedError | |
| def success( | |
| self, | |
| *, | |
| reward: float = 0.0, | |
| state_updates: Mapping[str, Any] | None = None, | |
| memory_updates: Mapping[str, Any] | None = None, | |
| message: str = "", | |
| metadata: Mapping[str, Any] | None = None, | |
| ) -> BehaviorExecution: | |
| """ | |
| Create a successful behavior execution result. | |
| Parameters | |
| ---------- | |
| reward: | |
| Scalar reward produced by the behavior. | |
| state_updates: | |
| Agent state updates to apply after execution. | |
| memory_updates: | |
| Memory payload to append to the agent after execution. | |
| message: | |
| Human-readable outcome message. | |
| metadata: | |
| Additional structured execution metadata. | |
| Returns | |
| ------- | |
| BehaviorExecution | |
| Successful normalized behavior result. | |
| """ | |
| return self._make_execution( | |
| success=True, | |
| reward=reward, | |
| state_updates=state_updates, | |
| memory_updates=memory_updates, | |
| message=message, | |
| metadata=metadata, | |
| ) | |
| def failure( | |
| self, | |
| *, | |
| reward: float = 0.0, | |
| state_updates: Mapping[str, Any] | None = None, | |
| memory_updates: Mapping[str, Any] | None = None, | |
| message: str = "", | |
| metadata: Mapping[str, Any] | None = None, | |
| ) -> BehaviorExecution: | |
| """ | |
| Create a failed behavior execution result. | |
| Failed results are useful when a behavior was selected and attempted but | |
| could not complete due to runtime conditions. | |
| Parameters | |
| ---------- | |
| reward: | |
| Scalar reward or penalty produced by the failed behavior. | |
| state_updates: | |
| Agent state updates to apply after failure. | |
| memory_updates: | |
| Memory payload to append to the agent after failure. | |
| message: | |
| Human-readable failure message. | |
| metadata: | |
| Additional structured execution metadata. | |
| Returns | |
| ------- | |
| BehaviorExecution | |
| Failed normalized behavior result. | |
| """ | |
| return self._make_execution( | |
| success=False, | |
| reward=reward, | |
| state_updates=state_updates, | |
| memory_updates=memory_updates, | |
| message=message, | |
| metadata=metadata, | |
| ) | |
| def get_parameter( | |
| self, | |
| name: str, | |
| default: T | object = _MISSING, | |
| *, | |
| expected_type: type[T] | tuple[type[Any], ...] | None = None, | |
| ) -> T: | |
| """ | |
| Retrieve a behavior parameter with optional runtime type validation. | |
| Parameters | |
| ---------- | |
| name: | |
| Parameter name. | |
| default: | |
| Default value returned when the parameter is absent. If omitted and | |
| the parameter is absent, ``KeyError`` is raised. | |
| expected_type: | |
| Optional type or tuple of types that the value must match. | |
| Returns | |
| ------- | |
| T | |
| Parameter value. | |
| Raises | |
| ------ | |
| KeyError | |
| If the parameter is missing and no default was provided. | |
| TypeError | |
| If the parameter name or value type is invalid. | |
| """ | |
| if not isinstance(name, str) or not name: | |
| raise TypeError("Parameter name must be a non-empty string.") | |
| if name in self.parameters: | |
| value = self.parameters[name] | |
| elif default is _MISSING: | |
| raise KeyError( | |
| f"Behavior '{self.id}' requires missing parameter '{name}'." | |
| ) | |
| else: | |
| value = default | |
| if expected_type is not None and not isinstance(value, expected_type): | |
| raise TypeError( | |
| f"Behavior '{self.id}' parameter '{name}' must be of type " | |
| f"{expected_type}; got {type(value).__name__}." | |
| ) | |
| return cast(T, value) | |
| def require_parameters(self, *names: str) -> None: | |
| """ | |
| Assert that required parameters are present and not ``None``. | |
| Parameters | |
| ---------- | |
| *names: | |
| Required parameter names. | |
| Raises | |
| ------ | |
| KeyError | |
| If one or more parameters are missing. | |
| TypeError | |
| If a parameter name is invalid. | |
| """ | |
| missing: list[str] = [] | |
| for name in names: | |
| if not isinstance(name, str) or not name: | |
| raise TypeError("Required parameter names must be non-empty strings.") | |
| if name not in self.parameters or self.parameters[name] is None: | |
| missing.append(name) | |
| if missing: | |
| joined = ", ".join(missing) | |
| raise KeyError( | |
| f"Behavior '{self.id}' is missing required parameter(s): {joined}." | |
| ) | |
| def has_agent_state(self, agent: Agent, *keys: str) -> bool: | |
| """ | |
| Return whether an agent has all requested state keys. | |
| Parameters | |
| ---------- | |
| agent: | |
| Agent whose state should be inspected. | |
| *keys: | |
| Required state keys. | |
| Returns | |
| ------- | |
| bool | |
| True if all keys are present and not ``None``. | |
| """ | |
| for key in keys: | |
| if not isinstance(key, str) or not key: | |
| raise TypeError("Agent state keys must be non-empty strings.") | |
| if key not in agent.state or agent.state[key] is None: | |
| return False | |
| return True | |
| def get_agent_state( | |
| self, | |
| agent: Agent, | |
| key: str, | |
| default: T | object = _MISSING, | |
| *, | |
| expected_type: type[T] | tuple[type[Any], ...] | None = None, | |
| ) -> T: | |
| """ | |
| Retrieve a value from an agent's state with optional validation. | |
| Parameters | |
| ---------- | |
| agent: | |
| Agent whose state should be inspected. | |
| key: | |
| State key. | |
| default: | |
| Default value returned when the key is absent. If omitted and the | |
| key is absent, ``KeyError`` is raised. | |
| expected_type: | |
| Optional type or tuple of types that the value must match. | |
| Returns | |
| ------- | |
| T | |
| State value. | |
| Raises | |
| ------ | |
| KeyError | |
| If the key is missing and no default was provided. | |
| TypeError | |
| If the key or value type is invalid. | |
| """ | |
| if not isinstance(key, str) or not key: | |
| raise TypeError("Agent state key must be a non-empty string.") | |
| if key in agent.state: | |
| value = agent.state[key] | |
| elif default is _MISSING: | |
| raise KeyError( | |
| f"Agent '{agent.id}' is missing required state key '{key}'." | |
| ) | |
| else: | |
| value = default | |
| if expected_type is not None and not isinstance(value, expected_type): | |
| raise TypeError( | |
| f"Agent '{agent.id}' state key '{key}' must be of type " | |
| f"{expected_type}; got {type(value).__name__}." | |
| ) | |
| return cast(T, value) | |
| def as_dict(self) -> dict[str, Any]: | |
| """ | |
| Return a JSON-friendly description of this behavior. | |
| Returns | |
| ------- | |
| dict[str, Any] | |
| Serializable behavior metadata. | |
| """ | |
| return { | |
| "id": self.id, | |
| "name": self.name, | |
| "class": self.__class__.__name__, | |
| "enabled": self.enabled, | |
| "parameters": deepcopy(self.parameters), | |
| "metadata": deepcopy(self.metadata), | |
| } | |
| def _make_execution( | |
| self, | |
| *, | |
| success: bool, | |
| reward: float, | |
| state_updates: Mapping[str, Any] | None, | |
| memory_updates: Mapping[str, Any] | None, | |
| message: str, | |
| metadata: Mapping[str, Any] | None, | |
| ) -> BehaviorExecution: | |
| """ | |
| Build a normalized ``BehaviorExecution`` object. | |
| Parameters | |
| ---------- | |
| success: | |
| Whether execution succeeded. | |
| reward: | |
| Scalar reward. | |
| state_updates: | |
| Optional state updates. | |
| memory_updates: | |
| Optional memory updates. | |
| message: | |
| Human-readable outcome message. | |
| metadata: | |
| Optional extra metadata. | |
| Returns | |
| ------- | |
| BehaviorExecution | |
| Normalized behavior execution result. | |
| """ | |
| return BehaviorExecution( | |
| success=success, | |
| reward=reward, | |
| state_updates=dict(state_updates or {}), | |
| memory_updates=dict(memory_updates or {}), | |
| message=message, | |
| metadata=self._build_execution_metadata(metadata), | |
| ) | |
| def _build_execution_metadata( | |
| self, | |
| metadata: Mapping[str, Any] | None, | |
| ) -> dict[str, Any]: | |
| """ | |
| Merge behavior metadata with execution-specific metadata. | |
| Parameters | |
| ---------- | |
| metadata: | |
| Optional metadata from a concrete behavior execution. | |
| Returns | |
| ------- | |
| dict[str, Any] | |
| Combined metadata dictionary. | |
| """ | |
| combined: dict[str, Any] = { | |
| "behavior_id": self.id, | |
| "behavior_name": self.name, | |
| "behavior_class": self.__class__.__name__, | |
| } | |
| combined.update(deepcopy(self.metadata)) | |
| if metadata is not None: | |
| if not isinstance(metadata, Mapping): | |
| raise TypeError("Execution metadata must be a mapping.") | |
| combined.update(dict(metadata)) | |
| return combined | |
| def euclidean_distance( | |
| left: PositionLike, | |
| right: PositionLike, | |
| ) -> float: | |
| """ | |
| Compute Euclidean distance between two numeric position vectors. | |
| This helper is intentionally generic and can be reused by movement, | |
| seeking, fleeing, trading, attack-range, communication-range, and | |
| collaboration-range behaviors. | |
| Parameters | |
| ---------- | |
| left: | |
| First position vector. | |
| right: | |
| Second position vector. | |
| Returns | |
| ------- | |
| float | |
| Euclidean distance. | |
| Raises | |
| ------ | |
| ValueError | |
| If either vector is invalid or the shapes do not match. | |
| """ | |
| left_vector = np.asarray(left, dtype=np.float64) | |
| right_vector = np.asarray(right, dtype=np.float64) | |
| if left_vector.ndim != 1: | |
| raise ValueError("left position must be a one-dimensional vector.") | |
| if right_vector.ndim != 1: | |
| raise ValueError("right position must be a one-dimensional vector.") | |
| if left_vector.shape != right_vector.shape: | |
| raise ValueError( | |
| "Position vectors must have the same dimensionality. " | |
| f"Got {left_vector.shape} and {right_vector.shape}." | |
| ) | |
| if not np.all(np.isfinite(left_vector)): | |
| raise ValueError("left position must contain only finite values.") | |
| if not np.all(np.isfinite(right_vector)): | |
| raise ValueError("right position must contain only finite values.") | |
| return float(np.linalg.norm(left_vector - right_vector)) | |
| def _validate_string_keys(mapping: Mapping[str, Any], label: str) -> None: | |
| """ | |
| Validate that all mapping keys are strings. | |
| Parameters | |
| ---------- | |
| mapping: | |
| Mapping to validate. | |
| label: | |
| Human-readable mapping label used in error messages. | |
| Raises | |
| ------ | |
| TypeError | |
| If any mapping key is not a string. | |
| """ | |
| for key in mapping: | |
| if not isinstance(key, str): | |
| raise TypeError(f"Behavior.{label} keys must be strings.") | |
| __all__ = [ | |
| "Behavior", | |
| "BehaviorExecution", | |
| "BehaviorResult", | |
| "PositionLike", | |
| ] |