WorldSmithAI / factory /world_factory.py
Srishti280992's picture
Upload 39 files
caad8d0 verified
Raw History Blame
44 kB
"""
Runtime object factory for WorldSmithAI.
This module converts validated DSL specifications into live runtime objects:
World, Agent, Resource, Event, Behavior, Policy, and Scheduler instances.
The factory is intentionally domain-agnostic. It does not know about sheep,
wolves, farms, dragons, scientists, markets, civilizations, power grids,
transport networks, or research ecosystems. It only resolves generic DSL
records through registries and runtime constructors.
Typical pipeline:
raw prompt
-> llm.world_generator
-> JSON DSL
-> dsl.parser
-> dsl.validator
-> WorldFactory
-> World
Example:
from dsl.parser import parse_world_file
from factory.world_factory import WorldFactory
spec = parse_world_file("examples/research.json")
world = WorldFactory().create_world(spec)
for _ in range(spec.simulation.steps):
world.step()
Future extensibility:
- Add plugin package discovery for external behavior registries.
- Add metric object instantiation once metrics registries are available.
- Add migration-aware factory profiles for schema versions.
- Add object pooling for large simulations.
- Add dependency injection hooks for testing.
- Add richer event adapters and world-level event buses.
"""
from __future__ import annotations
import copy
import inspect
import logging
from collections.abc import Mapping, MutableMapping, Sequence
from dataclasses import dataclass, field
from importlib import import_module
from types import MappingProxyType
from typing import Any
from core.agent import Agent
from core.behavior import Behavior
from core.event import Event
from core.resource import Resource
from core.scheduler import Scheduler
from core.world import World
from dsl.schema import (
AgentSpec,
BehaviorSpec,
EventSpec,
MetricSpec,
PolicySpec,
ResourceSpec,
WorldSpec,
)
from dsl.validator import (
ValidationConfig,
ValidationReport,
ValidationSeverity,
validate_world_spec,
)
from policies.base_policy import BasePolicy
logger = logging.getLogger(__name__)
DEFAULT_BEHAVIOR_MODULES: tuple[str, ...] = (
"behaviors.movement",
"behaviors.consume",
"behaviors.trade",
"behaviors.attack",
"behaviors.research",
"behaviors.collaboration",
"behaviors.transport",
"behaviors.construction",
"behaviors.governance",
"behaviors.market",
"behaviors.adoption",
"behaviors.planning",
"behaviors.memory",
)
DEFAULT_POLICY_MODULES: tuple[str, ...] = (
"policies.rule_policy",
"policies.contextual_bandit",
)
BEHAVIOR_REGISTRY_ATTRIBUTES: tuple[str, ...] = ("BEHAVIOR_REGISTRY",)
POLICY_REGISTRY_ATTRIBUTES: tuple[str, ...] = (
"POLICY_REGISTRY",
"RULE_POLICY_REGISTRY",
)
_CORE_AGENT_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType(
{
"id": ("agent_id",),
"type": ("agent_type", "kind"),
}
)
_CORE_RESOURCE_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType(
{
"id": ("resource_id",),
"type": ("resource_type", "kind"),
"amount": ("quantity",),
}
)
_CORE_EVENT_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType(
{
"id": ("event_id",),
"name": ("type", "event_type"),
"trigger_step": ("step", "at_step"),
"payload": ("data",),
}
)
_CORE_WORLD_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
_CORE_SCHEDULER_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
class WorldFactoryError(RuntimeError):
"""Base exception raised by the runtime world factory."""
class RegistryResolutionError(WorldFactoryError):
"""Raised when a behavior, policy, or runtime class cannot be resolved."""
class ConstructorBindingError(WorldFactoryError):
"""Raised when DSL parameters cannot be bound to a constructor."""
class RuntimeObjectBuildError(WorldFactoryError):
"""Raised when a runtime object cannot be built."""
class WorldFactoryValidationError(WorldFactoryError):
"""Raised when semantic DSL validation fails before world construction."""
def __init__(self, report: ValidationReport) -> None:
"""Initialize the error from a validation report."""
self.report = report
super().__init__(report.summary())
@dataclass(frozen=True)
class RegistryLoadReport:
"""Report describing dynamically loaded registry entries."""
registry: Mapping[str, Any]
import_errors: Mapping[str, str] = field(default_factory=dict)
@property
def names(self) -> tuple[str, ...]:
"""Return sorted registry names."""
return tuple(sorted(str(name) for name in self.registry.keys()))
def to_dict(self) -> dict[str, Any]:
"""Return a JSON-friendly registry load report."""
return {
"names": list(self.names),
"count": len(self.registry),
"import_errors": copy.deepcopy(dict(self.import_errors)),
}
@dataclass(frozen=True)
class WorldBuildReport:
"""Structured report generated during world construction."""
world_id: str
agent_ids: tuple[str, ...]
resource_ids: tuple[str, ...]
event_keys: tuple[str, ...]
behavior_registry_count: int
policy_registry_count: int
import_errors: Mapping[str, str] = field(default_factory=dict)
validation_report: ValidationReport | None = None
metadata: Mapping[str, Any] = field(default_factory=dict)
@property
def valid(self) -> bool:
"""Return whether the optional validation report is valid."""
return self.validation_report is None or self.validation_report.is_valid
def to_dict(self) -> dict[str, Any]:
"""Return a JSON-friendly build report."""
return {
"world_id": self.world_id,
"agent_ids": list(self.agent_ids),
"resource_ids": list(self.resource_ids),
"event_keys": list(self.event_keys),
"agent_count": len(self.agent_ids),
"resource_count": len(self.resource_ids),
"event_count": len(self.event_keys),
"behavior_registry_count": self.behavior_registry_count,
"policy_registry_count": self.policy_registry_count,
"import_errors": copy.deepcopy(dict(self.import_errors)),
"validation_report": None
if self.validation_report is None
else self.validation_report.to_dict(),
"metadata": copy.deepcopy(dict(self.metadata)),
}
@dataclass(frozen=True)
class WorldBuildResult:
"""Factory result containing the runtime world and build report."""
world: World
report: WorldBuildReport
@dataclass
class WorldFactoryConfig:
"""Configuration for ``WorldFactory``.
Attributes:
validate_before_build: Run semantic DSL validation before construction.
strict_unknown_behaviors: Raise if a behavior name is not registered.
strict_unknown_policies: Raise if a policy type is not registered.
strict_constructor_params: Raise on constructor parameters not accepted
by the runtime class.
include_disabled_behaviors: Instantiate behaviors marked disabled.
include_disabled_events: Instantiate events marked disabled.
attach_default_policy: Attach a default policy to agents that define
behaviors but omit a policy.
default_policy_type: Default policy registry key.
attach_scheduler: Attach a scheduler instance to the world when
possible.
attach_dsl_spec_to_world: Store the validated ``WorldSpec`` on the
runtime world for diagnostics and UI use.
"""
validate_before_build: bool = True
strict_unknown_behaviors: bool = True
strict_unknown_policies: bool = True
strict_constructor_params: bool = True
include_disabled_behaviors: bool = False
include_disabled_events: bool = False
attach_default_policy: bool = True
default_policy_type: str = "rule_policy"
attach_scheduler: bool = True
attach_dsl_spec_to_world: bool = True
behavior_modules: tuple[str, ...] = DEFAULT_BEHAVIOR_MODULES
policy_modules: tuple[str, ...] = DEFAULT_POLICY_MODULES
behavior_registry: Mapping[str, Any] | None = None
policy_registry: Mapping[str, Any] | None = None
agent_class: type[Agent] = Agent
resource_class: type[Resource] = Resource
event_class: type[Event] = Event
world_class: type[World] = World
scheduler_class: type[Scheduler] | None = Scheduler
world_uses_mapping_collections: bool = True
@dataclass
class GenericScheduledEvent:
"""Fallback event object used when ``core.event.Event`` is not instantiable.
The adapter keeps event records executable and inspectable without imposing
domain-specific event semantics. It returns its payload when executed.
Future event-system modules can replace this with richer event dispatch.
"""
name: str
trigger_step: int
payload: Mapping[str, Any] = field(default_factory=dict)
id: str | None = None
enabled: bool = True
repeat_interval: int | None = None
target_agent_ids: tuple[str, ...] = ()
target_resource_ids: tuple[str, ...] = ()
metadata: Mapping[str, Any] = field(default_factory=dict)
def execute(self, world: World) -> dict[str, Any]:
"""Execute the event in a domain-agnostic way.
The fallback adapter does not mutate world state by default. It returns
a structured event outcome that schedulers, logs, metrics, or narrators
can consume.
"""
return {
"event": self.name,
"event_id": self.id,
"trigger_step": self.trigger_step,
"enabled": self.enabled,
"payload": copy.deepcopy(dict(self.payload)),
"target_agent_ids": list(self.target_agent_ids),
"target_resource_ids": list(self.target_resource_ids),
"world_step": getattr(world, "step_count", None),
}
@dataclass
class WorldFactory:
"""Factory that converts ``WorldSpec`` into a runtime ``World``.
The factory owns behavior and policy registries. Registries may come from
default behavior/policy modules or from custom mappings supplied through
``WorldFactoryConfig``.
"""
config: WorldFactoryConfig = field(default_factory=WorldFactoryConfig)
_behavior_registry: dict[str, Any] = field(default_factory=dict, init=False, repr=False)
_policy_registry: dict[str, Any] = field(default_factory=dict, init=False, repr=False)
_registry_import_errors: dict[str, str] = field(default_factory=dict, init=False, repr=False)
def __post_init__(self) -> None:
"""Load configured behavior and policy registries."""
self._behavior_registry = self._build_behavior_registry()
self._policy_registry = self._build_policy_registry()
@property
def behavior_registry(self) -> Mapping[str, Any]:
"""Return a read-only view of the behavior registry."""
return MappingProxyType(self._behavior_registry)
@property
def policy_registry(self) -> Mapping[str, Any]:
"""Return a read-only view of the policy registry."""
return MappingProxyType(self._policy_registry)
@property
def registry_import_errors(self) -> Mapping[str, str]:
"""Return registry import errors collected during initialization."""
return MappingProxyType(self._registry_import_errors)
def create_world(self, spec: WorldSpec | Mapping[str, Any]) -> World:
"""Create a runtime world from a validated or mappable world spec.
Args:
spec: ``WorldSpec`` or mapping that can validate into ``WorldSpec``.
Returns:
Runtime ``World`` object.
Raises:
WorldFactoryValidationError: If semantic validation fails.
WorldFactoryError: If runtime construction fails.
"""
return self.create_world_result(spec).world
def create_world_result(self, spec: WorldSpec | Mapping[str, Any]) -> WorldBuildResult:
"""Create a world and return both runtime object and build report."""
world_spec = self._coerce_world_spec(spec)
validation_report = self._validate_before_build(world_spec)
resources = tuple(self.create_resource(resource_spec) for resource_spec in world_spec.resources)
events = tuple(
self.create_event(event_spec)
for event_spec in world_spec.events
if event_spec.enabled or self.config.include_disabled_events
)
agents = tuple(self.create_agent(agent_spec) for agent_spec in world_spec.agents)
metrics = tuple(self.create_metric_config(metric_spec) for metric_spec in world_spec.metrics)
world = self._create_world_object(
spec=world_spec,
agents=agents,
resources=resources,
events=events,
metrics=metrics,
)
if self.config.attach_scheduler:
self._attach_scheduler(world, world_spec)
self._attach_world_metadata(world, world_spec)
report = WorldBuildReport(
world_id=world_spec.id,
agent_ids=tuple(agent.id for agent in world_spec.agents),
resource_ids=tuple(resource.id for resource in world_spec.resources),
event_keys=tuple(event.event_key for event in world_spec.events),
behavior_registry_count=len(self._behavior_registry),
policy_registry_count=len(self._policy_registry),
import_errors=copy.deepcopy(self._registry_import_errors),
validation_report=validation_report,
metadata={
"world_name": world_spec.name,
"schema_version": world_spec.schema_version,
"simulation_steps": world_spec.simulation.steps,
},
)
logger.info(
"Created world %s with %s agent(s), %s resource(s), and %s event(s)",
world_spec.id,
len(agents),
len(resources),
len(events),
)
return WorldBuildResult(world=world, report=report)
def create_agent(self, spec: AgentSpec) -> Agent:
"""Create a runtime ``Agent`` from an ``AgentSpec``."""
behaviors = tuple(
self.create_behavior(behavior_spec)
for behavior_spec in spec.behaviors
if behavior_spec.enabled or self.config.include_disabled_behaviors
)
policy = self.create_policy_for_agent(spec)
params = {
"id": spec.id,
"type": spec.type,
"position": None if spec.position is None else tuple(spec.position),
"state": copy.deepcopy(spec.state),
"memory": copy.deepcopy(spec.memory),
"goals": copy.deepcopy(spec.goals),
"behaviors": behaviors,
"policy": policy,
"alive": bool(spec.alive),
}
agent = self._instantiate_runtime_object(
self.config.agent_class,
params,
aliases=_CORE_AGENT_ALIASES,
strict=False,
path=f"agents.{spec.id}",
item_kind="agent",
)
self._safe_setattr(agent, "id", spec.id)
self._safe_setattr(agent, "type", spec.type)
self._safe_setattr(agent, "position", None if spec.position is None else tuple(spec.position))
self._safe_setattr(agent, "state", copy.deepcopy(spec.state))
self._safe_setattr(agent, "memory", copy.deepcopy(spec.memory))
self._safe_setattr(agent, "goals", copy.deepcopy(spec.goals))
self._safe_setattr(agent, "behaviors", behaviors)
self._safe_setattr(agent, "policy", policy)
self._safe_setattr(agent, "alive", bool(spec.alive))
self._safe_setattr(agent, "metadata", copy.deepcopy(spec.metadata))
self._safe_setattr(agent, "dsl_spec", spec)
return agent
def create_behavior(self, spec: BehaviorSpec) -> Behavior:
"""Create a runtime behavior object from a ``BehaviorSpec``."""
target = self._resolve_registry_item(
registry=self._behavior_registry,
name=spec.name,
strict=self.config.strict_unknown_behaviors,
item_kind="behavior",
path=f"behavior:{spec.name}",
)
params = copy.deepcopy(spec.params)
behavior = self._instantiate_runtime_object(
target,
params,
aliases={},
strict=self.config.strict_constructor_params,
path=f"behavior:{spec.name}.params",
item_kind="behavior",
)
if not isinstance(behavior, Behavior) and not callable(getattr(behavior, "execute", None)):
raise RuntimeObjectBuildError(
f"Behavior {spec.name!r} did not produce an executable behavior object"
)
self._safe_setattr(behavior, "name", spec.name)
self._safe_setattr(behavior, "enabled", bool(spec.enabled))
self._safe_setattr(behavior, "priority", float(spec.priority))
self._safe_setattr(behavior, "tags", tuple(spec.tags))
self._safe_setattr(behavior, "metadata", copy.deepcopy(spec.metadata))
self._safe_setattr(behavior, "dsl_spec", spec)
return behavior
def create_policy_for_agent(self, agent_spec: AgentSpec) -> BasePolicy | None:
"""Create the policy configured for an agent.
If the agent has behaviors but omits a policy, the factory may attach a
default policy depending on configuration.
"""
policy_spec = agent_spec.policy
if policy_spec is None and self.config.attach_default_policy and agent_spec.behaviors:
policy_spec = PolicySpec(type=self.config.default_policy_type, params={})
if policy_spec is None:
return None
return self.create_policy(policy_spec)
def create_policy(self, spec: PolicySpec) -> BasePolicy:
"""Create a runtime policy object from a ``PolicySpec``."""
target = self._resolve_registry_item(
registry=self._policy_registry,
name=spec.type,
strict=self.config.strict_unknown_policies,
item_kind="policy",
path=f"policy:{spec.type}",
)
params = copy.deepcopy(spec.params)
params.setdefault("enabled", bool(spec.enabled))
if spec.metadata:
params.setdefault("metadata", copy.deepcopy(spec.metadata))
policy = self._instantiate_runtime_object(
target,
params,
aliases={},
strict=self.config.strict_constructor_params,
path=f"policy:{spec.type}.params",
item_kind="policy",
)
if not isinstance(policy, BasePolicy) and not callable(getattr(policy, "choose_action", None)):
raise RuntimeObjectBuildError(
f"Policy {spec.type!r} did not produce a valid policy object"
)
self._safe_setattr(policy, "enabled", bool(spec.enabled))
self._safe_setattr(policy, "metadata", copy.deepcopy(spec.metadata))
self._safe_setattr(policy, "dsl_spec", spec)
return policy
def create_resource(self, spec: ResourceSpec) -> Resource:
"""Create a runtime ``Resource`` from a ``ResourceSpec``."""
metadata = copy.deepcopy(spec.metadata)
metadata.setdefault("regeneration_rate", spec.regeneration_rate)
metadata.setdefault("max_amount", spec.max_amount)
params = {
"id": spec.id,
"type": spec.type,
"amount": float(spec.amount),
"position": None if spec.position is None else tuple(spec.position),
"metadata": metadata,
"regeneration_rate": float(spec.regeneration_rate),
"max_amount": spec.max_amount,
}
resource = self._instantiate_runtime_object(
self.config.resource_class,
params,
aliases=_CORE_RESOURCE_ALIASES,
strict=False,
path=f"resources.{spec.id}",
item_kind="resource",
)
self._safe_setattr(resource, "id", spec.id)
self._safe_setattr(resource, "type", spec.type)
self._safe_setattr(resource, "amount", float(spec.amount))
self._safe_setattr(resource, "position", None if spec.position is None else tuple(spec.position))
self._safe_setattr(resource, "metadata", metadata)
self._safe_setattr(resource, "regeneration_rate", float(spec.regeneration_rate))
self._safe_setattr(resource, "max_amount", spec.max_amount)
self._safe_setattr(resource, "dsl_spec", spec)
return resource
def create_event(self, spec: EventSpec) -> Event | GenericScheduledEvent:
"""Create a runtime event object from an ``EventSpec``."""
params = {
"id": spec.id,
"name": spec.name,
"trigger_step": int(spec.trigger_step),
"payload": copy.deepcopy(spec.payload),
"enabled": bool(spec.enabled),
"repeat_interval": spec.repeat_interval,
"target_agent_ids": tuple(spec.target_agent_ids),
"target_resource_ids": tuple(spec.target_resource_ids),
"metadata": copy.deepcopy(spec.metadata),
}
event_class = self.config.event_class
use_fallback = inspect.isabstract(event_class)
if use_fallback:
event: Event | GenericScheduledEvent = GenericScheduledEvent(**params)
else:
try:
event = self._instantiate_runtime_object(
event_class,
params,
aliases=_CORE_EVENT_ALIASES,
strict=False,
path=f"events.{spec.event_key}",
item_kind="event",
)
except WorldFactoryError:
logger.debug(
"Falling back to GenericScheduledEvent for event %s",
spec.event_key,
exc_info=True,
)
event = GenericScheduledEvent(**params)
self._safe_setattr(event, "id", spec.id)
self._safe_setattr(event, "name", spec.name)
self._safe_setattr(event, "trigger_step", int(spec.trigger_step))
self._safe_setattr(event, "payload", copy.deepcopy(spec.payload))
self._safe_setattr(event, "enabled", bool(spec.enabled))
self._safe_setattr(event, "repeat_interval", spec.repeat_interval)
self._safe_setattr(event, "target_agent_ids", tuple(spec.target_agent_ids))
self._safe_setattr(event, "target_resource_ids", tuple(spec.target_resource_ids))
self._safe_setattr(event, "metadata", copy.deepcopy(spec.metadata))
self._safe_setattr(event, "dsl_spec", spec)
return event
def create_metric_config(self, spec: MetricSpec) -> dict[str, Any]:
"""Create a metric configuration record.
Metric runtime classes are introduced later in the implementation
order. Until then, metric specs are preserved as serializable records on
the world.
"""
return {
"name": spec.name,
"params": copy.deepcopy(spec.params),
"enabled": bool(spec.enabled),
"metadata": copy.deepcopy(spec.metadata),
}
def _create_world_object(
self,
*,
spec: WorldSpec,
agents: Sequence[Agent],
resources: Sequence[Resource],
events: Sequence[Event | GenericScheduledEvent],
metrics: Sequence[Mapping[str, Any]],
) -> World:
"""Instantiate and populate the runtime world object."""
agent_collection: Mapping[str, Agent] | tuple[Agent, ...]
resource_collection: Mapping[str, Resource] | tuple[Resource, ...]
if self.config.world_uses_mapping_collections:
agent_collection = {str(getattr(agent, "id")): agent for agent in agents}
resource_collection = {str(getattr(resource, "id")): resource for resource in resources}
else:
agent_collection = tuple(agents)
resource_collection = tuple(resources)
metric_collection = {
str(metric["name"]): copy.deepcopy(dict(metric))
for metric in metrics
if "name" in metric
}
params = {
"agents": agent_collection,
"resources": resource_collection,
"events": tuple(events),
"metrics": metric_collection,
"step_count": 0,
}
try:
world = self._instantiate_runtime_object(
self.config.world_class,
params,
aliases=_CORE_WORLD_ALIASES,
strict=False,
path=f"world:{spec.id}",
item_kind="world",
)
except WorldFactoryError:
logger.debug("Falling back to no-argument World constructor", exc_info=True)
world = self.config.world_class()
self._install_agents(world, agents)
self._install_resources(world, resources)
self._install_events(world, events)
self._safe_setattr(world, "metrics", metric_collection)
self._safe_setattr(world, "step_count", 0)
return world
def _install_agents(self, world: World, agents: Sequence[Agent]) -> None:
"""Install agents into the world using adders when available."""
if self.config.world_uses_mapping_collections:
self._safe_setattr(world, "agents", {})
else:
self._safe_setattr(world, "agents", [])
add_agent = getattr(world, "add_agent", None)
if callable(add_agent):
for agent in agents:
add_agent(agent)
return
if self.config.world_uses_mapping_collections:
self._safe_setattr(world, "agents", {str(getattr(agent, "id")): agent for agent in agents})
else:
self._safe_setattr(world, "agents", list(agents))
def _install_resources(self, world: World, resources: Sequence[Resource]) -> None:
"""Install resources into the world using adders when available."""
if self.config.world_uses_mapping_collections:
self._safe_setattr(world, "resources", {})
else:
self._safe_setattr(world, "resources", [])
add_resource = getattr(world, "add_resource", None)
if callable(add_resource):
for resource in resources:
add_resource(resource)
return
if self.config.world_uses_mapping_collections:
self._safe_setattr(
world,
"resources",
{str(getattr(resource, "id")): resource for resource in resources},
)
else:
self._safe_setattr(world, "resources", list(resources))
def _install_events(
self,
world: World,
events: Sequence[Event | GenericScheduledEvent],
) -> None:
"""Install events into the world using adders when available."""
self._safe_setattr(world, "events", [])
add_event = getattr(world, "add_event", None)
if callable(add_event):
for event in events:
add_event(event)
return
self._safe_setattr(world, "events", list(events))
def _attach_scheduler(self, world: World, spec: WorldSpec) -> None:
"""Attach a scheduler to the world when a scheduler class is configured."""
scheduler_class = self.config.scheduler_class
if scheduler_class is None:
return
params = {
"world": world,
"activation": spec.simulation.activation,
"scheduler": spec.simulation.scheduler,
"seed": spec.simulation.seed,
}
try:
scheduler = self._instantiate_runtime_object(
scheduler_class,
params,
aliases=_CORE_SCHEDULER_ALIASES,
strict=False,
path=f"world:{spec.id}.scheduler",
item_kind="scheduler",
)
except WorldFactoryError:
logger.debug("Falling back to no-argument Scheduler constructor", exc_info=True)
scheduler = scheduler_class()
self._safe_setattr(scheduler, "world", world)
self._safe_setattr(scheduler, "activation", spec.simulation.activation)
self._safe_setattr(scheduler, "scheduler", spec.simulation.scheduler)
self._safe_setattr(scheduler, "seed", spec.simulation.seed)
self._safe_setattr(world, "scheduler", scheduler)
def _attach_world_metadata(self, world: World, spec: WorldSpec) -> None:
"""Attach DSL metadata and simulation configuration to the runtime world."""
self._safe_setattr(world, "id", spec.id)
self._safe_setattr(world, "name", spec.name)
self._safe_setattr(world, "description", spec.description)
self._safe_setattr(world, "metadata", copy.deepcopy(spec.metadata))
self._safe_setattr(world, "simulation", spec.simulation)
self._safe_setattr(world, "simulation_config", spec.simulation.to_dict())
self._safe_setattr(world, "space", spec.space)
self._safe_setattr(world, "space_config", None if spec.space is None else spec.space.to_dict())
self._safe_setattr(world, "schema_version", spec.schema_version)
if self.config.attach_dsl_spec_to_world:
self._safe_setattr(world, "dsl_spec", spec)
def _validate_before_build(self, spec: WorldSpec) -> ValidationReport | None:
"""Optionally run semantic validation before object construction."""
if not self.config.validate_before_build:
return None
validation_config = ValidationConfig(
behavior_registry=self._behavior_registry,
policy_registry=self._policy_registry,
require_known_behaviors=self.config.strict_unknown_behaviors,
require_known_policies=self.config.strict_unknown_policies,
validate_constructor_params=self.config.strict_constructor_params,
unknown_registry_item_severity=ValidationSeverity.ERROR
if self.config.strict_unknown_behaviors or self.config.strict_unknown_policies
else ValidationSeverity.WARNING,
constructor_param_severity=ValidationSeverity.ERROR
if self.config.strict_constructor_params
else ValidationSeverity.WARNING,
)
report = validate_world_spec(spec, config=validation_config)
if not report.is_valid:
raise WorldFactoryValidationError(report)
return report
def _coerce_world_spec(self, spec: WorldSpec | Mapping[str, Any]) -> WorldSpec:
"""Return a ``WorldSpec`` from a spec-like object."""
if isinstance(spec, WorldSpec):
return spec
if isinstance(spec, Mapping):
return WorldSpec.model_validate(dict(spec))
raise TypeError(f"Expected WorldSpec or mapping, got {spec.__class__.__name__}")
def _build_behavior_registry(self) -> dict[str, Any]:
"""Build the behavior registry from defaults and custom entries."""
registry: dict[str, Any] = {}
module_report = self._load_module_registries(
self.config.behavior_modules,
BEHAVIOR_REGISTRY_ATTRIBUTES,
base_class=Behavior,
)
registry.update(module_report.registry)
self._registry_import_errors.update(module_report.import_errors)
if self.config.behavior_registry is not None:
registry.update({str(key): value for key, value in self.config.behavior_registry.items()})
return self._with_class_aliases(registry)
def _build_policy_registry(self) -> dict[str, Any]:
"""Build the policy registry from defaults and custom entries."""
registry: dict[str, Any] = {}
module_report = self._load_module_registries(
self.config.policy_modules,
POLICY_REGISTRY_ATTRIBUTES,
base_class=BasePolicy,
)
registry.update(module_report.registry)
self._registry_import_errors.update(module_report.import_errors)
if self.config.policy_registry is not None:
registry.update({str(key): value for key, value in self.config.policy_registry.items()})
return self._with_class_aliases(registry)
def _load_module_registries(
self,
module_names: Sequence[str],
registry_attributes: Sequence[str],
*,
base_class: type[Any],
) -> RegistryLoadReport:
"""Load registry mappings from modules and discover named subclasses."""
registry: dict[str, Any] = {}
import_errors: dict[str, str] = {}
for module_name in module_names:
try:
module = import_module(module_name)
except Exception as exc: # pragma: no cover - defensive for user modules
import_errors[str(module_name)] = f"{exc.__class__.__name__}: {exc}"
continue
for attribute_name in registry_attributes:
raw_registry = getattr(module, attribute_name, None)
if isinstance(raw_registry, Mapping):
registry.update({str(key): value for key, value in raw_registry.items()})
registry.update(self._discover_named_subclasses(module, base_class=base_class))
return RegistryLoadReport(registry=registry, import_errors=import_errors)
@staticmethod
def _discover_named_subclasses(module: Any, *, base_class: type[Any]) -> dict[str, Any]:
"""Discover subclasses with a string ``name`` attribute from a module."""
discovered: dict[str, Any] = {}
for attribute_value in vars(module).values():
if not inspect.isclass(attribute_value):
continue
if attribute_value is base_class:
continue
try:
is_subclass = issubclass(attribute_value, base_class)
except TypeError:
is_subclass = False
if not is_subclass:
continue
name = getattr(attribute_value, "name", None)
if isinstance(name, str) and name:
discovered.setdefault(name, attribute_value)
return discovered
@staticmethod
def _with_class_aliases(registry: Mapping[str, Any]) -> dict[str, Any]:
"""Add class-name and ``name`` aliases to registry entries."""
expanded = {str(key): value for key, value in registry.items()}
for key, value in list(expanded.items()):
class_name = getattr(value, "__name__", None)
if class_name:
expanded.setdefault(str(class_name), value)
runtime_name = getattr(value, "name", None)
if isinstance(runtime_name, str) and runtime_name:
expanded.setdefault(runtime_name, value)
expanded.setdefault(str(key).lower(), value)
return expanded
def _resolve_registry_item(
self,
*,
registry: Mapping[str, Any],
name: str,
strict: bool,
item_kind: str,
path: str,
) -> Any:
"""Resolve a registry item by exact or case-insensitive name."""
if name in registry:
return registry[name]
lower_name = name.lower()
if lower_name in registry:
return registry[lower_name]
for registered_name, item in registry.items():
if registered_name.lower() == lower_name:
return item
message = (
f"Unknown {item_kind} {name!r} at {path}. "
f"Known {item_kind}s: {sorted(str(key) for key in registry.keys())}"
)
if strict:
raise RegistryResolutionError(message)
logger.warning(message)
return self._noop_behavior_factory(name) if item_kind == "behavior" else self._noop_policy_factory(name)
def _instantiate_runtime_object(
self,
target: Any,
params: Mapping[str, Any],
*,
aliases: Mapping[str, Sequence[str]],
strict: bool,
path: str,
item_kind: str,
) -> Any:
"""Instantiate a runtime object using signature-aware keyword binding."""
if not callable(target):
try:
return copy.deepcopy(target)
except Exception as exc:
raise RuntimeObjectBuildError(
f"Registered {item_kind} at {path} is not callable and could not be copied"
) from exc
kwargs = self._bind_constructor_kwargs(
target,
params,
aliases=aliases,
strict=strict,
path=path,
item_kind=item_kind,
)
try:
return target(**kwargs)
except TypeError as exc:
raise ConstructorBindingError(
f"Could not instantiate {item_kind} at {path}: {exc}"
) from exc
except Exception as exc:
raise RuntimeObjectBuildError(
f"Constructor for {item_kind} at {path} failed: {exc}"
) from exc
def _bind_constructor_kwargs(
self,
target: Any,
params: Mapping[str, Any],
*,
aliases: Mapping[str, Sequence[str]],
strict: bool,
path: str,
item_kind: str,
) -> dict[str, Any]:
"""Filter and alias params according to a target constructor signature."""
working = copy.deepcopy(dict(params))
signature_info = self._constructor_signature_info(target)
if signature_info is None:
return working
allowed_params, required_params, accepts_var_keyword = signature_info
if accepts_var_keyword:
return working
for canonical_name, alias_names in aliases.items():
if canonical_name not in working:
continue
if canonical_name in allowed_params:
continue
for alias_name in alias_names:
if alias_name in allowed_params and alias_name not in working:
working[alias_name] = working[canonical_name]
break
unknown_params = set(working.keys()) - allowed_params
if unknown_params and strict:
raise ConstructorBindingError(
f"Unknown constructor parameter(s) for {item_kind} at {path}: "
f"{sorted(unknown_params)}. Allowed parameters: {sorted(allowed_params)}"
)
filtered = {
key: value
for key, value in working.items()
if key in allowed_params
}
missing_required = required_params - set(filtered.keys())
if missing_required:
raise ConstructorBindingError(
f"Missing required constructor parameter(s) for {item_kind} at {path}: "
f"{sorted(missing_required)}"
)
return filtered
@staticmethod
def _constructor_signature_info(target: Any) -> tuple[set[str], set[str], bool] | None:
"""Return constructor binding information for a callable target."""
try:
signature = inspect.signature(target)
except (TypeError, ValueError):
return None
allowed_params: set[str] = set()
required_params: set[str] = set()
accepts_var_keyword = False
for name, parameter in signature.parameters.items():
if name == "self":
continue
if parameter.kind is inspect.Parameter.VAR_KEYWORD:
accepts_var_keyword = True
continue
if parameter.kind is inspect.Parameter.VAR_POSITIONAL:
continue
if parameter.kind not in {
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
}:
continue
allowed_params.add(name)
if parameter.default is inspect.Parameter.empty:
required_params.add(name)
return allowed_params, required_params, accepts_var_keyword
@staticmethod
def _safe_setattr(instance: Any, name: str, value: Any) -> bool:
"""Set an attribute defensively.
Returns:
``True`` if the attribute was set, otherwise ``False``.
"""
try:
setattr(instance, name, value)
return True
except Exception:
logger.debug(
"Could not set attribute %s on %s",
name,
instance.__class__.__name__,
exc_info=True,
)
return False
@staticmethod
def _noop_behavior_factory(name: str) -> type[Behavior]:
"""Create a no-op behavior class for permissive unknown behavior mode."""
class NoOpBehavior(Behavior):
"""Fallback behavior used only when unknown behaviors are permissive."""
behavior_name = name
def check_preconditions(self, agent: Agent, world: World) -> bool:
"""Return False so unknown behavior never executes."""
del agent, world
return False
def execute(self, agent: Agent, world: World) -> dict[str, Any]:
"""Return a structured no-op result."""
del world
return {
"behavior": self.behavior_name,
"actor_id": getattr(agent, "id", None),
"success": False,
"details": {"reason": "unknown_behavior_noop"},
}
NoOpBehavior.name = name # type: ignore[attr-defined]
return NoOpBehavior
@staticmethod
def _noop_policy_factory(name: str) -> type[BasePolicy]:
"""Create a no-op policy class for permissive unknown policy mode."""
class NoOpPolicy(BasePolicy):
"""Fallback policy used only when unknown policies are permissive."""
policy_name_value = name
def choose_action(
self,
agent: Agent,
world: World,
behaviors: Sequence[Behavior] | Mapping[str, Behavior] | None = None,
) -> Behavior | None:
"""Choose no behavior."""
del agent, world, behaviors
return None
NoOpPolicy.name = name # type: ignore[attr-defined]
return NoOpPolicy
def create_world(
spec: WorldSpec | Mapping[str, Any],
*,
config: WorldFactoryConfig | None = None,
) -> World:
"""Convenience function that creates a runtime world from a spec."""
return WorldFactory(config=config or WorldFactoryConfig()).create_world(spec)
def create_world_result(
spec: WorldSpec | Mapping[str, Any],
*,
config: WorldFactoryConfig | None = None,
) -> WorldBuildResult:
"""Convenience function that creates a world and returns its build report."""
return WorldFactory(config=config or WorldFactoryConfig()).create_world_result(spec)
__all__ = [
"BEHAVIOR_REGISTRY_ATTRIBUTES",
"DEFAULT_BEHAVIOR_MODULES",
"DEFAULT_POLICY_MODULES",
"GenericScheduledEvent",
"ConstructorBindingError",
"POLICY_REGISTRY_ATTRIBUTES",
"RegistryLoadReport",
"RegistryResolutionError",
"RuntimeObjectBuildError",
"WorldBuildReport",
"WorldBuildResult",
"WorldFactory",
"WorldFactoryConfig",
"WorldFactoryError",
"WorldFactoryValidationError",
"create_world",
"create_world_result",
]