Spaces:
Runtime error
Runtime error
Update factory/world_factory.py
Browse files- factory/world_factory.py +317 -169
factory/world_factory.py
CHANGED
|
@@ -18,23 +18,13 @@ Typical pipeline:
|
|
| 18 |
-> WorldFactory
|
| 19 |
-> World
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
for _ in range(spec.simulation.steps):
|
| 29 |
-
world.step()
|
| 30 |
-
|
| 31 |
-
Future extensibility:
|
| 32 |
-
- Add plugin package discovery for external behavior registries.
|
| 33 |
-
- Add metric object instantiation once metrics registries are available.
|
| 34 |
-
- Add migration-aware factory profiles for schema versions.
|
| 35 |
-
- Add object pooling for large simulations.
|
| 36 |
-
- Add dependency injection hooks for testing.
|
| 37 |
-
- Add richer event adapters and world-level event buses.
|
| 38 |
"""
|
| 39 |
|
| 40 |
from __future__ import annotations
|
|
@@ -42,13 +32,14 @@ from __future__ import annotations
|
|
| 42 |
import copy
|
| 43 |
import inspect
|
| 44 |
import logging
|
| 45 |
-
|
| 46 |
-
from collections.abc import Mapping, MutableMapping, Sequence
|
| 47 |
from dataclasses import dataclass, field
|
| 48 |
from importlib import import_module
|
| 49 |
from types import MappingProxyType
|
| 50 |
from typing import Any
|
| 51 |
|
|
|
|
|
|
|
| 52 |
from core.agent import Agent
|
| 53 |
from core.behavior import Behavior
|
| 54 |
from core.event import Event
|
|
@@ -72,8 +63,10 @@ from dsl.validator import (
|
|
| 72 |
)
|
| 73 |
from policies.base_policy import BasePolicy
|
| 74 |
|
|
|
|
| 75 |
logger = logging.getLogger(__name__)
|
| 76 |
|
|
|
|
| 77 |
DEFAULT_BEHAVIOR_MODULES: tuple[str, ...] = (
|
| 78 |
"behaviors.movement",
|
| 79 |
"behaviors.consume",
|
|
@@ -96,6 +89,7 @@ DEFAULT_POLICY_MODULES: tuple[str, ...] = (
|
|
| 96 |
)
|
| 97 |
|
| 98 |
BEHAVIOR_REGISTRY_ATTRIBUTES: tuple[str, ...] = ("BEHAVIOR_REGISTRY",)
|
|
|
|
| 99 |
POLICY_REGISTRY_ATTRIBUTES: tuple[str, ...] = (
|
| 100 |
"POLICY_REGISTRY",
|
| 101 |
"RULE_POLICY_REGISTRY",
|
|
@@ -126,6 +120,7 @@ _CORE_EVENT_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType(
|
|
| 126 |
)
|
| 127 |
|
| 128 |
_CORE_WORLD_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
|
|
|
|
| 129 |
_CORE_SCHEDULER_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
|
| 130 |
|
| 131 |
|
|
@@ -229,24 +224,7 @@ class WorldBuildResult:
|
|
| 229 |
|
| 230 |
@dataclass
|
| 231 |
class WorldFactoryConfig:
|
| 232 |
-
"""Configuration for ``WorldFactory``.
|
| 233 |
-
|
| 234 |
-
Attributes:
|
| 235 |
-
validate_before_build: Run semantic DSL validation before construction.
|
| 236 |
-
strict_unknown_behaviors: Raise if a behavior name is not registered.
|
| 237 |
-
strict_unknown_policies: Raise if a policy type is not registered.
|
| 238 |
-
strict_constructor_params: Raise on constructor parameters not accepted
|
| 239 |
-
by the runtime class.
|
| 240 |
-
include_disabled_behaviors: Instantiate behaviors marked disabled.
|
| 241 |
-
include_disabled_events: Instantiate events marked disabled.
|
| 242 |
-
attach_default_policy: Attach a default policy to agents that define
|
| 243 |
-
behaviors but omit a policy.
|
| 244 |
-
default_policy_type: Default policy registry key.
|
| 245 |
-
attach_scheduler: Attach a scheduler instance to the world when
|
| 246 |
-
possible.
|
| 247 |
-
attach_dsl_spec_to_world: Store the validated ``WorldSpec`` on the
|
| 248 |
-
runtime world for diagnostics and UI use.
|
| 249 |
-
"""
|
| 250 |
|
| 251 |
validate_before_build: bool = True
|
| 252 |
|
|
@@ -280,12 +258,7 @@ class WorldFactoryConfig:
|
|
| 280 |
|
| 281 |
@dataclass
|
| 282 |
class GenericScheduledEvent:
|
| 283 |
-
"""Fallback event object used when ``core.event.Event`` is not instantiable.
|
| 284 |
-
|
| 285 |
-
The adapter keeps event records executable and inspectable without imposing
|
| 286 |
-
domain-specific event semantics. It returns its payload when executed.
|
| 287 |
-
Future event-system modules can replace this with richer event dispatch.
|
| 288 |
-
"""
|
| 289 |
|
| 290 |
name: str
|
| 291 |
trigger_step: int
|
|
@@ -298,12 +271,7 @@ class GenericScheduledEvent:
|
|
| 298 |
metadata: Mapping[str, Any] = field(default_factory=dict)
|
| 299 |
|
| 300 |
def execute(self, world: World) -> dict[str, Any]:
|
| 301 |
-
"""Execute the event in a domain-agnostic way.
|
| 302 |
-
|
| 303 |
-
The fallback adapter does not mutate world state by default. It returns
|
| 304 |
-
a structured event outcome that schedulers, logs, metrics, or narrators
|
| 305 |
-
can consume.
|
| 306 |
-
"""
|
| 307 |
|
| 308 |
return {
|
| 309 |
"event": self.name,
|
|
@@ -319,12 +287,7 @@ class GenericScheduledEvent:
|
|
| 319 |
|
| 320 |
@dataclass
|
| 321 |
class WorldFactory:
|
| 322 |
-
"""Factory that converts ``WorldSpec`` into a runtime ``World``.
|
| 323 |
-
|
| 324 |
-
The factory owns behavior and policy registries. Registries may come from
|
| 325 |
-
default behavior/policy modules or from custom mappings supplied through
|
| 326 |
-
``WorldFactoryConfig``.
|
| 327 |
-
"""
|
| 328 |
|
| 329 |
config: WorldFactoryConfig = field(default_factory=WorldFactoryConfig)
|
| 330 |
|
|
@@ -355,69 +318,9 @@ class WorldFactory:
|
|
| 355 |
"""Return registry import errors collected during initialization."""
|
| 356 |
|
| 357 |
return MappingProxyType(self._registry_import_errors)
|
| 358 |
-
|
| 359 |
-
@staticmethod
|
| 360 |
-
def _behavior_runtime_id(
|
| 361 |
-
spec: BehaviorSpec,
|
| 362 |
-
*,
|
| 363 |
-
owner_agent_id: str | None,
|
| 364 |
-
behavior_index: int | None,
|
| 365 |
-
) -> str:
|
| 366 |
-
"""Return a deterministic runtime id for a behavior spec."""
|
| 367 |
-
|
| 368 |
-
metadata_id = None
|
| 369 |
-
if isinstance(spec.metadata, Mapping):
|
| 370 |
-
metadata_id = spec.metadata.get("id") or spec.metadata.get("behavior_id")
|
| 371 |
-
|
| 372 |
-
if metadata_id is not None and str(metadata_id).strip():
|
| 373 |
-
return str(metadata_id).strip()
|
| 374 |
-
|
| 375 |
-
owner = owner_agent_id or "world"
|
| 376 |
-
index = 0 if behavior_index is None else int(behavior_index)
|
| 377 |
-
return f"{owner}:behavior:{index}:{spec.name}"
|
| 378 |
-
|
| 379 |
-
@staticmethod
|
| 380 |
-
def _agent_runtime_position(position: Any) -> np.ndarray:
|
| 381 |
-
"""Return a NumPy position vector compatible with core.agent.Agent.
|
| 382 |
-
|
| 383 |
-
The current Agent implementation expects position to behave like a NumPy
|
| 384 |
-
array. In particular, Agent.observe() and Agent.snapshot() use array-style
|
| 385 |
-
methods such as copy() and tolist().
|
| 386 |
-
"""
|
| 387 |
-
|
| 388 |
-
if position is None:
|
| 389 |
-
return np.zeros(2, dtype=np.float64)
|
| 390 |
-
|
| 391 |
-
try:
|
| 392 |
-
array = np.asarray(position, dtype=np.float64).reshape(-1)
|
| 393 |
-
except (TypeError, ValueError) as exc:
|
| 394 |
-
raise RuntimeObjectBuildError(
|
| 395 |
-
f"Agent position must be numeric, got {position!r}"
|
| 396 |
-
) from exc
|
| 397 |
-
|
| 398 |
-
if array.size == 0:
|
| 399 |
-
return np.zeros(2, dtype=np.float64)
|
| 400 |
-
|
| 401 |
-
if not np.all(np.isfinite(array)):
|
| 402 |
-
raise RuntimeObjectBuildError(
|
| 403 |
-
f"Agent position must contain only finite values, got {position!r}"
|
| 404 |
-
)
|
| 405 |
-
|
| 406 |
-
return array
|
| 407 |
|
| 408 |
def create_world(self, spec: WorldSpec | Mapping[str, Any]) -> World:
|
| 409 |
-
"""Create a runtime world from a validated or mappable world spec.
|
| 410 |
-
|
| 411 |
-
Args:
|
| 412 |
-
spec: ``WorldSpec`` or mapping that can validate into ``WorldSpec``.
|
| 413 |
-
|
| 414 |
-
Returns:
|
| 415 |
-
Runtime ``World`` object.
|
| 416 |
-
|
| 417 |
-
Raises:
|
| 418 |
-
WorldFactoryValidationError: If semantic validation fails.
|
| 419 |
-
WorldFactoryError: If runtime construction fails.
|
| 420 |
-
"""
|
| 421 |
|
| 422 |
return self.create_world_result(spec).world
|
| 423 |
|
|
@@ -427,14 +330,32 @@ class WorldFactory:
|
|
| 427 |
world_spec = self._coerce_world_spec(spec)
|
| 428 |
validation_report = self._validate_before_build(world_spec)
|
| 429 |
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
events = tuple(
|
| 432 |
self.create_event(event_spec)
|
| 433 |
for event_spec in world_spec.events
|
| 434 |
if event_spec.enabled or self.config.include_disabled_events
|
| 435 |
)
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
world = self._create_world_object(
|
| 440 |
spec=world_spec,
|
|
@@ -475,32 +396,59 @@ class WorldFactory:
|
|
| 475 |
|
| 476 |
return WorldBuildResult(world=world, report=report)
|
| 477 |
|
| 478 |
-
def create_agent(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
"""Create a runtime ``Agent`` from an ``AgentSpec``."""
|
| 480 |
|
| 481 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
behavior_spec,
|
| 484 |
owner_agent_id=spec.id,
|
| 485 |
behavior_index=behavior_index,
|
| 486 |
)
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
policy = self.create_policy_for_agent(spec)
|
| 491 |
|
| 492 |
-
runtime_position = self._agent_runtime_position(
|
|
|
|
|
|
|
|
|
|
| 493 |
|
| 494 |
params = {
|
| 495 |
"id": spec.id,
|
| 496 |
"type": spec.type,
|
| 497 |
-
"position": runtime_position,
|
| 498 |
"state": copy.deepcopy(spec.state),
|
| 499 |
"memory": copy.deepcopy(spec.memory),
|
| 500 |
"goals": copy.deepcopy(spec.goals),
|
| 501 |
-
"behaviors":
|
| 502 |
"policy": policy,
|
| 503 |
"alive": bool(spec.alive),
|
|
|
|
|
|
|
| 504 |
}
|
| 505 |
|
| 506 |
agent = self._instantiate_runtime_object(
|
|
@@ -514,16 +462,17 @@ class WorldFactory:
|
|
| 514 |
|
| 515 |
self._safe_setattr(agent, "id", spec.id)
|
| 516 |
self._safe_setattr(agent, "type", spec.type)
|
| 517 |
-
self.
|
| 518 |
self._safe_setattr(agent, "state", copy.deepcopy(spec.state))
|
| 519 |
self._safe_setattr(agent, "memory", copy.deepcopy(spec.memory))
|
| 520 |
self._safe_setattr(agent, "goals", copy.deepcopy(spec.goals))
|
| 521 |
-
self._safe_setattr(agent, "behaviors",
|
| 522 |
self._safe_setattr(agent, "policy", policy)
|
| 523 |
self._safe_setattr(agent, "alive", bool(spec.alive))
|
| 524 |
self._safe_setattr(agent, "metadata", copy.deepcopy(spec.metadata))
|
| 525 |
self._safe_setattr(agent, "dsl_spec", spec)
|
| 526 |
|
|
|
|
| 527 |
return agent
|
| 528 |
|
| 529 |
def create_behavior(
|
|
@@ -533,12 +482,7 @@ class WorldFactory:
|
|
| 533 |
owner_agent_id: str | None = None,
|
| 534 |
behavior_index: int | None = None,
|
| 535 |
) -> Behavior:
|
| 536 |
-
"""Create a runtime behavior object from a ``BehaviorSpec``.
|
| 537 |
-
|
| 538 |
-
The DSL does not require behavior ids because behavior specs are declarative.
|
| 539 |
-
Runtime ids are injected here so behavior base classes that require ``id``
|
| 540 |
-
can still be instantiated safely and deterministically.
|
| 541 |
-
"""
|
| 542 |
|
| 543 |
target = self._resolve_registry_item(
|
| 544 |
registry=self._behavior_registry,
|
|
@@ -555,9 +499,6 @@ class WorldFactory:
|
|
| 555 |
)
|
| 556 |
|
| 557 |
params = copy.deepcopy(spec.params)
|
| 558 |
-
|
| 559 |
-
# Runtime-injected fields. These are filtered out automatically if the
|
| 560 |
-
# concrete behavior constructor does not accept them.
|
| 561 |
params.setdefault("id", runtime_behavior_id)
|
| 562 |
params.setdefault("behavior_id", runtime_behavior_id)
|
| 563 |
params.setdefault("name", spec.name)
|
|
@@ -587,11 +528,7 @@ class WorldFactory:
|
|
| 587 |
return behavior
|
| 588 |
|
| 589 |
def create_policy_for_agent(self, agent_spec: AgentSpec) -> BasePolicy | None:
|
| 590 |
-
"""Create the policy configured for an agent.
|
| 591 |
-
|
| 592 |
-
If the agent has behaviors but omits a policy, the factory may attach a
|
| 593 |
-
default policy depending on configuration.
|
| 594 |
-
"""
|
| 595 |
|
| 596 |
policy_spec = agent_spec.policy
|
| 597 |
|
|
@@ -616,6 +553,7 @@ class WorldFactory:
|
|
| 616 |
|
| 617 |
params = copy.deepcopy(spec.params)
|
| 618 |
params.setdefault("enabled", bool(spec.enabled))
|
|
|
|
| 619 |
if spec.metadata:
|
| 620 |
params.setdefault("metadata", copy.deepcopy(spec.metadata))
|
| 621 |
|
|
@@ -639,21 +577,32 @@ class WorldFactory:
|
|
| 639 |
|
| 640 |
return policy
|
| 641 |
|
| 642 |
-
def create_resource(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
"""Create a runtime ``Resource`` from a ``ResourceSpec``."""
|
| 644 |
|
| 645 |
metadata = copy.deepcopy(spec.metadata)
|
| 646 |
metadata.setdefault("regeneration_rate", spec.regeneration_rate)
|
| 647 |
metadata.setdefault("max_amount", spec.max_amount)
|
| 648 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 649 |
params = {
|
| 650 |
"id": spec.id,
|
| 651 |
"type": spec.type,
|
| 652 |
"amount": float(spec.amount),
|
| 653 |
-
"position":
|
| 654 |
"metadata": metadata,
|
| 655 |
"regeneration_rate": float(spec.regeneration_rate),
|
| 656 |
"max_amount": spec.max_amount,
|
|
|
|
| 657 |
}
|
| 658 |
|
| 659 |
resource = self._instantiate_runtime_object(
|
|
@@ -668,12 +617,13 @@ class WorldFactory:
|
|
| 668 |
self._safe_setattr(resource, "id", spec.id)
|
| 669 |
self._safe_setattr(resource, "type", spec.type)
|
| 670 |
self._safe_setattr(resource, "amount", float(spec.amount))
|
| 671 |
-
self.
|
| 672 |
self._safe_setattr(resource, "metadata", metadata)
|
| 673 |
self._safe_setattr(resource, "regeneration_rate", float(spec.regeneration_rate))
|
| 674 |
self._safe_setattr(resource, "max_amount", spec.max_amount)
|
| 675 |
self._safe_setattr(resource, "dsl_spec", spec)
|
| 676 |
|
|
|
|
| 677 |
return resource
|
| 678 |
|
| 679 |
def create_event(self, spec: EventSpec) -> Event | GenericScheduledEvent:
|
|
@@ -728,12 +678,7 @@ class WorldFactory:
|
|
| 728 |
return event
|
| 729 |
|
| 730 |
def create_metric_config(self, spec: MetricSpec) -> dict[str, Any]:
|
| 731 |
-
"""Create a metric configuration record.
|
| 732 |
-
|
| 733 |
-
Metric runtime classes are introduced later in the implementation
|
| 734 |
-
order. Until then, metric specs are preserved as serializable records on
|
| 735 |
-
the world.
|
| 736 |
-
"""
|
| 737 |
|
| 738 |
return {
|
| 739 |
"name": spec.name,
|
|
@@ -753,12 +698,13 @@ class WorldFactory:
|
|
| 753 |
) -> World:
|
| 754 |
"""Instantiate and populate the runtime world object."""
|
| 755 |
|
| 756 |
-
agent_collection: Mapping[str, Agent] | tuple[Agent, ...]
|
| 757 |
-
resource_collection: Mapping[str, Resource] | tuple[Resource, ...]
|
| 758 |
-
|
| 759 |
if self.config.world_uses_mapping_collections:
|
| 760 |
-
agent_collection
|
| 761 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 762 |
else:
|
| 763 |
agent_collection = tuple(agents)
|
| 764 |
resource_collection = tuple(resources)
|
|
@@ -775,6 +721,7 @@ class WorldFactory:
|
|
| 775 |
"events": tuple(events),
|
| 776 |
"metrics": metric_collection,
|
| 777 |
"step_count": 0,
|
|
|
|
| 778 |
}
|
| 779 |
|
| 780 |
try:
|
|
@@ -790,9 +737,14 @@ class WorldFactory:
|
|
| 790 |
logger.debug("Falling back to no-argument World constructor", exc_info=True)
|
| 791 |
world = self.config.world_class()
|
| 792 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 793 |
self._install_agents(world, agents)
|
| 794 |
self._install_resources(world, resources)
|
| 795 |
self._install_events(world, events)
|
|
|
|
| 796 |
self._safe_setattr(world, "metrics", metric_collection)
|
| 797 |
self._safe_setattr(world, "step_count", 0)
|
| 798 |
|
|
@@ -807,14 +759,24 @@ class WorldFactory:
|
|
| 807 |
self._safe_setattr(world, "agents", [])
|
| 808 |
|
| 809 |
add_agent = getattr(world, "add_agent", None)
|
|
|
|
| 810 |
if callable(add_agent):
|
| 811 |
for agent in agents:
|
|
|
|
| 812 |
add_agent(agent)
|
| 813 |
return
|
| 814 |
|
| 815 |
if self.config.world_uses_mapping_collections:
|
| 816 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
else:
|
|
|
|
|
|
|
| 818 |
self._safe_setattr(world, "agents", list(agents))
|
| 819 |
|
| 820 |
def _install_resources(self, world: World, resources: Sequence[Resource]) -> None:
|
|
@@ -826,18 +788,24 @@ class WorldFactory:
|
|
| 826 |
self._safe_setattr(world, "resources", [])
|
| 827 |
|
| 828 |
add_resource = getattr(world, "add_resource", None)
|
|
|
|
| 829 |
if callable(add_resource):
|
| 830 |
for resource in resources:
|
|
|
|
| 831 |
add_resource(resource)
|
| 832 |
return
|
| 833 |
|
| 834 |
if self.config.world_uses_mapping_collections:
|
|
|
|
|
|
|
| 835 |
self._safe_setattr(
|
| 836 |
world,
|
| 837 |
"resources",
|
| 838 |
{str(getattr(resource, "id")): resource for resource in resources},
|
| 839 |
)
|
| 840 |
else:
|
|
|
|
|
|
|
| 841 |
self._safe_setattr(world, "resources", list(resources))
|
| 842 |
|
| 843 |
def _install_events(
|
|
@@ -850,6 +818,7 @@ class WorldFactory:
|
|
| 850 |
self._safe_setattr(world, "events", [])
|
| 851 |
|
| 852 |
add_event = getattr(world, "add_event", None)
|
|
|
|
| 853 |
if callable(add_event):
|
| 854 |
for event in events:
|
| 855 |
add_event(event)
|
|
@@ -861,6 +830,7 @@ class WorldFactory:
|
|
| 861 |
"""Attach a scheduler to the world when a scheduler class is configured."""
|
| 862 |
|
| 863 |
scheduler_class = self.config.scheduler_class
|
|
|
|
| 864 |
if scheduler_class is None:
|
| 865 |
return
|
| 866 |
|
|
@@ -903,6 +873,11 @@ class WorldFactory:
|
|
| 903 |
self._safe_setattr(world, "space_config", None if spec.space is None else spec.space.to_dict())
|
| 904 |
self._safe_setattr(world, "schema_version", spec.schema_version)
|
| 905 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 906 |
if self.config.attach_dsl_spec_to_world:
|
| 907 |
self._safe_setattr(world, "dsl_spec", spec)
|
| 908 |
|
|
@@ -925,6 +900,7 @@ class WorldFactory:
|
|
| 925 |
if self.config.strict_constructor_params
|
| 926 |
else ValidationSeverity.WARNING,
|
| 927 |
)
|
|
|
|
| 928 |
report = validate_world_spec(spec, config=validation_config)
|
| 929 |
|
| 930 |
if not report.is_valid:
|
|
@@ -957,7 +933,10 @@ class WorldFactory:
|
|
| 957 |
self._registry_import_errors.update(module_report.import_errors)
|
| 958 |
|
| 959 |
if self.config.behavior_registry is not None:
|
| 960 |
-
registry.update({
|
|
|
|
|
|
|
|
|
|
| 961 |
|
| 962 |
return self._with_class_aliases(registry)
|
| 963 |
|
|
@@ -975,7 +954,10 @@ class WorldFactory:
|
|
| 975 |
self._registry_import_errors.update(module_report.import_errors)
|
| 976 |
|
| 977 |
if self.config.policy_registry is not None:
|
| 978 |
-
registry.update({
|
|
|
|
|
|
|
|
|
|
| 979 |
|
| 980 |
return self._with_class_aliases(registry)
|
| 981 |
|
|
@@ -994,16 +976,21 @@ class WorldFactory:
|
|
| 994 |
for module_name in module_names:
|
| 995 |
try:
|
| 996 |
module = import_module(module_name)
|
| 997 |
-
except Exception as exc:
|
| 998 |
import_errors[str(module_name)] = f"{exc.__class__.__name__}: {exc}"
|
| 999 |
continue
|
| 1000 |
|
| 1001 |
for attribute_name in registry_attributes:
|
| 1002 |
raw_registry = getattr(module, attribute_name, None)
|
| 1003 |
if isinstance(raw_registry, Mapping):
|
| 1004 |
-
registry.update({
|
|
|
|
|
|
|
|
|
|
| 1005 |
|
| 1006 |
-
registry.update(
|
|
|
|
|
|
|
| 1007 |
|
| 1008 |
return RegistryLoadReport(registry=registry, import_errors=import_errors)
|
| 1009 |
|
|
@@ -1084,7 +1071,11 @@ class WorldFactory:
|
|
| 1084 |
raise RegistryResolutionError(message)
|
| 1085 |
|
| 1086 |
logger.warning(message)
|
| 1087 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1088 |
|
| 1089 |
def _instantiate_runtime_object(
|
| 1090 |
self,
|
|
@@ -1162,6 +1153,7 @@ class WorldFactory:
|
|
| 1162 |
break
|
| 1163 |
|
| 1164 |
unknown_params = set(working.keys()) - allowed_params
|
|
|
|
| 1165 |
if unknown_params and strict:
|
| 1166 |
raise ConstructorBindingError(
|
| 1167 |
f"Unknown constructor parameter(s) for {item_kind} at {path}: "
|
|
@@ -1175,6 +1167,7 @@ class WorldFactory:
|
|
| 1175 |
}
|
| 1176 |
|
| 1177 |
missing_required = required_params - set(filtered.keys())
|
|
|
|
| 1178 |
if missing_required:
|
| 1179 |
raise ConstructorBindingError(
|
| 1180 |
f"Missing required constructor parameter(s) for {item_kind} at {path}: "
|
|
@@ -1221,13 +1214,150 @@ class WorldFactory:
|
|
| 1221 |
return allowed_params, required_params, accepts_var_keyword
|
| 1222 |
|
| 1223 |
@staticmethod
|
| 1224 |
-
def
|
| 1225 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1226 |
|
| 1227 |
-
|
| 1228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1229 |
"""
|
| 1230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1231 |
try:
|
| 1232 |
setattr(instance, name, value)
|
| 1233 |
return True
|
|
@@ -1240,6 +1370,24 @@ class WorldFactory:
|
|
| 1240 |
)
|
| 1241 |
return False
|
| 1242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1243 |
@staticmethod
|
| 1244 |
def _noop_behavior_factory(name: str) -> type[Behavior]:
|
| 1245 |
"""Create a no-op behavior class for permissive unknown behavior mode."""
|
|
|
|
| 18 |
-> WorldFactory
|
| 19 |
-> World
|
| 20 |
|
| 21 |
+
Important runtime compatibility notes:
|
| 22 |
+
- DSL positions are JSON arrays, but runtime World/Agent/Resource code may
|
| 23 |
+
expect NumPy arrays with attributes such as ``.size``.
|
| 24 |
+
- This factory converts all runtime agent/resource positions to NumPy arrays.
|
| 25 |
+
- Behavior ids are runtime details and are injected automatically.
|
| 26 |
+
- Agent behavior storage is provided as a mapping keyed by deterministic
|
| 27 |
+
behavior ids.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
"""
|
| 29 |
|
| 30 |
from __future__ import annotations
|
|
|
|
| 32 |
import copy
|
| 33 |
import inspect
|
| 34 |
import logging
|
| 35 |
+
from collections.abc import Mapping, Sequence
|
|
|
|
| 36 |
from dataclasses import dataclass, field
|
| 37 |
from importlib import import_module
|
| 38 |
from types import MappingProxyType
|
| 39 |
from typing import Any
|
| 40 |
|
| 41 |
+
import numpy as np
|
| 42 |
+
|
| 43 |
from core.agent import Agent
|
| 44 |
from core.behavior import Behavior
|
| 45 |
from core.event import Event
|
|
|
|
| 63 |
)
|
| 64 |
from policies.base_policy import BasePolicy
|
| 65 |
|
| 66 |
+
|
| 67 |
logger = logging.getLogger(__name__)
|
| 68 |
|
| 69 |
+
|
| 70 |
DEFAULT_BEHAVIOR_MODULES: tuple[str, ...] = (
|
| 71 |
"behaviors.movement",
|
| 72 |
"behaviors.consume",
|
|
|
|
| 89 |
)
|
| 90 |
|
| 91 |
BEHAVIOR_REGISTRY_ATTRIBUTES: tuple[str, ...] = ("BEHAVIOR_REGISTRY",)
|
| 92 |
+
|
| 93 |
POLICY_REGISTRY_ATTRIBUTES: tuple[str, ...] = (
|
| 94 |
"POLICY_REGISTRY",
|
| 95 |
"RULE_POLICY_REGISTRY",
|
|
|
|
| 120 |
)
|
| 121 |
|
| 122 |
_CORE_WORLD_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
|
| 123 |
+
|
| 124 |
_CORE_SCHEDULER_ALIASES: Mapping[str, tuple[str, ...]] = MappingProxyType({})
|
| 125 |
|
| 126 |
|
|
|
|
| 224 |
|
| 225 |
@dataclass
|
| 226 |
class WorldFactoryConfig:
|
| 227 |
+
"""Configuration for ``WorldFactory``."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
validate_before_build: bool = True
|
| 230 |
|
|
|
|
| 258 |
|
| 259 |
@dataclass
|
| 260 |
class GenericScheduledEvent:
|
| 261 |
+
"""Fallback event object used when ``core.event.Event`` is not instantiable."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
name: str
|
| 264 |
trigger_step: int
|
|
|
|
| 271 |
metadata: Mapping[str, Any] = field(default_factory=dict)
|
| 272 |
|
| 273 |
def execute(self, world: World) -> dict[str, Any]:
|
| 274 |
+
"""Execute the event in a domain-agnostic way."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
return {
|
| 277 |
"event": self.name,
|
|
|
|
| 287 |
|
| 288 |
@dataclass
|
| 289 |
class WorldFactory:
|
| 290 |
+
"""Factory that converts ``WorldSpec`` into a runtime ``World``."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
config: WorldFactoryConfig = field(default_factory=WorldFactoryConfig)
|
| 293 |
|
|
|
|
| 318 |
"""Return registry import errors collected during initialization."""
|
| 319 |
|
| 320 |
return MappingProxyType(self._registry_import_errors)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
def create_world(self, spec: WorldSpec | Mapping[str, Any]) -> World:
|
| 323 |
+
"""Create a runtime world from a validated or mappable world spec."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
|
| 325 |
return self.create_world_result(spec).world
|
| 326 |
|
|
|
|
| 330 |
world_spec = self._coerce_world_spec(spec)
|
| 331 |
validation_report = self._validate_before_build(world_spec)
|
| 332 |
|
| 333 |
+
space_dimensions = (
|
| 334 |
+
int(world_spec.space.dimensions)
|
| 335 |
+
if world_spec.space is not None
|
| 336 |
+
else None
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
resources = tuple(
|
| 340 |
+
self.create_resource(resource_spec, dimensions=space_dimensions)
|
| 341 |
+
for resource_spec in world_spec.resources
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
events = tuple(
|
| 345 |
self.create_event(event_spec)
|
| 346 |
for event_spec in world_spec.events
|
| 347 |
if event_spec.enabled or self.config.include_disabled_events
|
| 348 |
)
|
| 349 |
+
|
| 350 |
+
agents = tuple(
|
| 351 |
+
self.create_agent(agent_spec, dimensions=space_dimensions)
|
| 352 |
+
for agent_spec in world_spec.agents
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
metrics = tuple(
|
| 356 |
+
self.create_metric_config(metric_spec)
|
| 357 |
+
for metric_spec in world_spec.metrics
|
| 358 |
+
)
|
| 359 |
|
| 360 |
world = self._create_world_object(
|
| 361 |
spec=world_spec,
|
|
|
|
| 396 |
|
| 397 |
return WorldBuildResult(world=world, report=report)
|
| 398 |
|
| 399 |
+
def create_agent(
|
| 400 |
+
self,
|
| 401 |
+
spec: AgentSpec,
|
| 402 |
+
*,
|
| 403 |
+
dimensions: int | None = None,
|
| 404 |
+
) -> Agent:
|
| 405 |
"""Create a runtime ``Agent`` from an ``AgentSpec``."""
|
| 406 |
|
| 407 |
+
behavior_map: dict[str, Behavior] = {}
|
| 408 |
+
|
| 409 |
+
for behavior_index, behavior_spec in enumerate(spec.behaviors):
|
| 410 |
+
if not behavior_spec.enabled and not self.config.include_disabled_behaviors:
|
| 411 |
+
continue
|
| 412 |
+
|
| 413 |
+
behavior = self.create_behavior(
|
| 414 |
behavior_spec,
|
| 415 |
owner_agent_id=spec.id,
|
| 416 |
behavior_index=behavior_index,
|
| 417 |
)
|
| 418 |
+
|
| 419 |
+
behavior_id = str(
|
| 420 |
+
getattr(behavior, "id", "")
|
| 421 |
+
or self._behavior_runtime_id(
|
| 422 |
+
behavior_spec,
|
| 423 |
+
owner_agent_id=spec.id,
|
| 424 |
+
behavior_index=behavior_index,
|
| 425 |
+
)
|
| 426 |
+
)
|
| 427 |
+
|
| 428 |
+
if behavior_id in behavior_map:
|
| 429 |
+
behavior_id = f"{behavior_id}#{behavior_index}"
|
| 430 |
+
|
| 431 |
+
behavior_map[behavior_id] = behavior
|
| 432 |
+
|
| 433 |
policy = self.create_policy_for_agent(spec)
|
| 434 |
|
| 435 |
+
runtime_position = self._agent_runtime_position(
|
| 436 |
+
spec.position,
|
| 437 |
+
dimensions=dimensions,
|
| 438 |
+
)
|
| 439 |
|
| 440 |
params = {
|
| 441 |
"id": spec.id,
|
| 442 |
"type": spec.type,
|
| 443 |
+
"position": runtime_position.copy(),
|
| 444 |
"state": copy.deepcopy(spec.state),
|
| 445 |
"memory": copy.deepcopy(spec.memory),
|
| 446 |
"goals": copy.deepcopy(spec.goals),
|
| 447 |
+
"behaviors": behavior_map,
|
| 448 |
"policy": policy,
|
| 449 |
"alive": bool(spec.alive),
|
| 450 |
+
"metadata": copy.deepcopy(spec.metadata),
|
| 451 |
+
"dsl_spec": spec,
|
| 452 |
}
|
| 453 |
|
| 454 |
agent = self._instantiate_runtime_object(
|
|
|
|
| 462 |
|
| 463 |
self._safe_setattr(agent, "id", spec.id)
|
| 464 |
self._safe_setattr(agent, "type", spec.type)
|
| 465 |
+
self._force_setattr(agent, "position", runtime_position.copy())
|
| 466 |
self._safe_setattr(agent, "state", copy.deepcopy(spec.state))
|
| 467 |
self._safe_setattr(agent, "memory", copy.deepcopy(spec.memory))
|
| 468 |
self._safe_setattr(agent, "goals", copy.deepcopy(spec.goals))
|
| 469 |
+
self._safe_setattr(agent, "behaviors", behavior_map)
|
| 470 |
self._safe_setattr(agent, "policy", policy)
|
| 471 |
self._safe_setattr(agent, "alive", bool(spec.alive))
|
| 472 |
self._safe_setattr(agent, "metadata", copy.deepcopy(spec.metadata))
|
| 473 |
self._safe_setattr(agent, "dsl_spec", spec)
|
| 474 |
|
| 475 |
+
self._ensure_agent_position_array(agent, dimensions=dimensions)
|
| 476 |
return agent
|
| 477 |
|
| 478 |
def create_behavior(
|
|
|
|
| 482 |
owner_agent_id: str | None = None,
|
| 483 |
behavior_index: int | None = None,
|
| 484 |
) -> Behavior:
|
| 485 |
+
"""Create a runtime behavior object from a ``BehaviorSpec``."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 486 |
|
| 487 |
target = self._resolve_registry_item(
|
| 488 |
registry=self._behavior_registry,
|
|
|
|
| 499 |
)
|
| 500 |
|
| 501 |
params = copy.deepcopy(spec.params)
|
|
|
|
|
|
|
|
|
|
| 502 |
params.setdefault("id", runtime_behavior_id)
|
| 503 |
params.setdefault("behavior_id", runtime_behavior_id)
|
| 504 |
params.setdefault("name", spec.name)
|
|
|
|
| 528 |
return behavior
|
| 529 |
|
| 530 |
def create_policy_for_agent(self, agent_spec: AgentSpec) -> BasePolicy | None:
|
| 531 |
+
"""Create the policy configured for an agent."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
|
| 533 |
policy_spec = agent_spec.policy
|
| 534 |
|
|
|
|
| 553 |
|
| 554 |
params = copy.deepcopy(spec.params)
|
| 555 |
params.setdefault("enabled", bool(spec.enabled))
|
| 556 |
+
|
| 557 |
if spec.metadata:
|
| 558 |
params.setdefault("metadata", copy.deepcopy(spec.metadata))
|
| 559 |
|
|
|
|
| 577 |
|
| 578 |
return policy
|
| 579 |
|
| 580 |
+
def create_resource(
|
| 581 |
+
self,
|
| 582 |
+
spec: ResourceSpec,
|
| 583 |
+
*,
|
| 584 |
+
dimensions: int | None = None,
|
| 585 |
+
) -> Resource:
|
| 586 |
"""Create a runtime ``Resource`` from a ``ResourceSpec``."""
|
| 587 |
|
| 588 |
metadata = copy.deepcopy(spec.metadata)
|
| 589 |
metadata.setdefault("regeneration_rate", spec.regeneration_rate)
|
| 590 |
metadata.setdefault("max_amount", spec.max_amount)
|
| 591 |
|
| 592 |
+
runtime_position = self._resource_runtime_position(
|
| 593 |
+
spec.position,
|
| 594 |
+
dimensions=dimensions,
|
| 595 |
+
)
|
| 596 |
+
|
| 597 |
params = {
|
| 598 |
"id": spec.id,
|
| 599 |
"type": spec.type,
|
| 600 |
"amount": float(spec.amount),
|
| 601 |
+
"position": runtime_position.copy(),
|
| 602 |
"metadata": metadata,
|
| 603 |
"regeneration_rate": float(spec.regeneration_rate),
|
| 604 |
"max_amount": spec.max_amount,
|
| 605 |
+
"dsl_spec": spec,
|
| 606 |
}
|
| 607 |
|
| 608 |
resource = self._instantiate_runtime_object(
|
|
|
|
| 617 |
self._safe_setattr(resource, "id", spec.id)
|
| 618 |
self._safe_setattr(resource, "type", spec.type)
|
| 619 |
self._safe_setattr(resource, "amount", float(spec.amount))
|
| 620 |
+
self._force_setattr(resource, "position", runtime_position.copy())
|
| 621 |
self._safe_setattr(resource, "metadata", metadata)
|
| 622 |
self._safe_setattr(resource, "regeneration_rate", float(spec.regeneration_rate))
|
| 623 |
self._safe_setattr(resource, "max_amount", spec.max_amount)
|
| 624 |
self._safe_setattr(resource, "dsl_spec", spec)
|
| 625 |
|
| 626 |
+
self._ensure_resource_position_array(resource, dimensions=dimensions)
|
| 627 |
return resource
|
| 628 |
|
| 629 |
def create_event(self, spec: EventSpec) -> Event | GenericScheduledEvent:
|
|
|
|
| 678 |
return event
|
| 679 |
|
| 680 |
def create_metric_config(self, spec: MetricSpec) -> dict[str, Any]:
|
| 681 |
+
"""Create a metric configuration record."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 682 |
|
| 683 |
return {
|
| 684 |
"name": spec.name,
|
|
|
|
| 698 |
) -> World:
|
| 699 |
"""Instantiate and populate the runtime world object."""
|
| 700 |
|
|
|
|
|
|
|
|
|
|
| 701 |
if self.config.world_uses_mapping_collections:
|
| 702 |
+
agent_collection: Mapping[str, Agent] | tuple[Agent, ...] = {
|
| 703 |
+
str(getattr(agent, "id")): agent for agent in agents
|
| 704 |
+
}
|
| 705 |
+
resource_collection: Mapping[str, Resource] | tuple[Resource, ...] = {
|
| 706 |
+
str(getattr(resource, "id")): resource for resource in resources
|
| 707 |
+
}
|
| 708 |
else:
|
| 709 |
agent_collection = tuple(agents)
|
| 710 |
resource_collection = tuple(resources)
|
|
|
|
| 721 |
"events": tuple(events),
|
| 722 |
"metrics": metric_collection,
|
| 723 |
"step_count": 0,
|
| 724 |
+
"metadata": copy.deepcopy(spec.metadata),
|
| 725 |
}
|
| 726 |
|
| 727 |
try:
|
|
|
|
| 737 |
logger.debug("Falling back to no-argument World constructor", exc_info=True)
|
| 738 |
world = self.config.world_class()
|
| 739 |
|
| 740 |
+
# Attach metadata before installation so World.add_agent/add_resource can
|
| 741 |
+
# see space/dimension configuration if the World implementation uses it.
|
| 742 |
+
self._attach_world_metadata(world, spec)
|
| 743 |
+
|
| 744 |
self._install_agents(world, agents)
|
| 745 |
self._install_resources(world, resources)
|
| 746 |
self._install_events(world, events)
|
| 747 |
+
|
| 748 |
self._safe_setattr(world, "metrics", metric_collection)
|
| 749 |
self._safe_setattr(world, "step_count", 0)
|
| 750 |
|
|
|
|
| 759 |
self._safe_setattr(world, "agents", [])
|
| 760 |
|
| 761 |
add_agent = getattr(world, "add_agent", None)
|
| 762 |
+
|
| 763 |
if callable(add_agent):
|
| 764 |
for agent in agents:
|
| 765 |
+
self._ensure_agent_position_array(agent)
|
| 766 |
add_agent(agent)
|
| 767 |
return
|
| 768 |
|
| 769 |
if self.config.world_uses_mapping_collections:
|
| 770 |
+
for agent in agents:
|
| 771 |
+
self._ensure_agent_position_array(agent)
|
| 772 |
+
self._safe_setattr(
|
| 773 |
+
world,
|
| 774 |
+
"agents",
|
| 775 |
+
{str(getattr(agent, "id")): agent for agent in agents},
|
| 776 |
+
)
|
| 777 |
else:
|
| 778 |
+
for agent in agents:
|
| 779 |
+
self._ensure_agent_position_array(agent)
|
| 780 |
self._safe_setattr(world, "agents", list(agents))
|
| 781 |
|
| 782 |
def _install_resources(self, world: World, resources: Sequence[Resource]) -> None:
|
|
|
|
| 788 |
self._safe_setattr(world, "resources", [])
|
| 789 |
|
| 790 |
add_resource = getattr(world, "add_resource", None)
|
| 791 |
+
|
| 792 |
if callable(add_resource):
|
| 793 |
for resource in resources:
|
| 794 |
+
self._ensure_resource_position_array(resource)
|
| 795 |
add_resource(resource)
|
| 796 |
return
|
| 797 |
|
| 798 |
if self.config.world_uses_mapping_collections:
|
| 799 |
+
for resource in resources:
|
| 800 |
+
self._ensure_resource_position_array(resource)
|
| 801 |
self._safe_setattr(
|
| 802 |
world,
|
| 803 |
"resources",
|
| 804 |
{str(getattr(resource, "id")): resource for resource in resources},
|
| 805 |
)
|
| 806 |
else:
|
| 807 |
+
for resource in resources:
|
| 808 |
+
self._ensure_resource_position_array(resource)
|
| 809 |
self._safe_setattr(world, "resources", list(resources))
|
| 810 |
|
| 811 |
def _install_events(
|
|
|
|
| 818 |
self._safe_setattr(world, "events", [])
|
| 819 |
|
| 820 |
add_event = getattr(world, "add_event", None)
|
| 821 |
+
|
| 822 |
if callable(add_event):
|
| 823 |
for event in events:
|
| 824 |
add_event(event)
|
|
|
|
| 830 |
"""Attach a scheduler to the world when a scheduler class is configured."""
|
| 831 |
|
| 832 |
scheduler_class = self.config.scheduler_class
|
| 833 |
+
|
| 834 |
if scheduler_class is None:
|
| 835 |
return
|
| 836 |
|
|
|
|
| 873 |
self._safe_setattr(world, "space_config", None if spec.space is None else spec.space.to_dict())
|
| 874 |
self._safe_setattr(world, "schema_version", spec.schema_version)
|
| 875 |
|
| 876 |
+
if spec.space is not None:
|
| 877 |
+
self._safe_setattr(world, "dimensions", int(spec.space.dimensions))
|
| 878 |
+
self._safe_setattr(world, "dimension", int(spec.space.dimensions))
|
| 879 |
+
self._safe_setattr(world, "space_dimensions", int(spec.space.dimensions))
|
| 880 |
+
|
| 881 |
if self.config.attach_dsl_spec_to_world:
|
| 882 |
self._safe_setattr(world, "dsl_spec", spec)
|
| 883 |
|
|
|
|
| 900 |
if self.config.strict_constructor_params
|
| 901 |
else ValidationSeverity.WARNING,
|
| 902 |
)
|
| 903 |
+
|
| 904 |
report = validate_world_spec(spec, config=validation_config)
|
| 905 |
|
| 906 |
if not report.is_valid:
|
|
|
|
| 933 |
self._registry_import_errors.update(module_report.import_errors)
|
| 934 |
|
| 935 |
if self.config.behavior_registry is not None:
|
| 936 |
+
registry.update({
|
| 937 |
+
str(key): value
|
| 938 |
+
for key, value in self.config.behavior_registry.items()
|
| 939 |
+
})
|
| 940 |
|
| 941 |
return self._with_class_aliases(registry)
|
| 942 |
|
|
|
|
| 954 |
self._registry_import_errors.update(module_report.import_errors)
|
| 955 |
|
| 956 |
if self.config.policy_registry is not None:
|
| 957 |
+
registry.update({
|
| 958 |
+
str(key): value
|
| 959 |
+
for key, value in self.config.policy_registry.items()
|
| 960 |
+
})
|
| 961 |
|
| 962 |
return self._with_class_aliases(registry)
|
| 963 |
|
|
|
|
| 976 |
for module_name in module_names:
|
| 977 |
try:
|
| 978 |
module = import_module(module_name)
|
| 979 |
+
except Exception as exc:
|
| 980 |
import_errors[str(module_name)] = f"{exc.__class__.__name__}: {exc}"
|
| 981 |
continue
|
| 982 |
|
| 983 |
for attribute_name in registry_attributes:
|
| 984 |
raw_registry = getattr(module, attribute_name, None)
|
| 985 |
if isinstance(raw_registry, Mapping):
|
| 986 |
+
registry.update({
|
| 987 |
+
str(key): value
|
| 988 |
+
for key, value in raw_registry.items()
|
| 989 |
+
})
|
| 990 |
|
| 991 |
+
registry.update(
|
| 992 |
+
self._discover_named_subclasses(module, base_class=base_class)
|
| 993 |
+
)
|
| 994 |
|
| 995 |
return RegistryLoadReport(registry=registry, import_errors=import_errors)
|
| 996 |
|
|
|
|
| 1071 |
raise RegistryResolutionError(message)
|
| 1072 |
|
| 1073 |
logger.warning(message)
|
| 1074 |
+
|
| 1075 |
+
if item_kind == "behavior":
|
| 1076 |
+
return self._noop_behavior_factory(name)
|
| 1077 |
+
|
| 1078 |
+
return self._noop_policy_factory(name)
|
| 1079 |
|
| 1080 |
def _instantiate_runtime_object(
|
| 1081 |
self,
|
|
|
|
| 1153 |
break
|
| 1154 |
|
| 1155 |
unknown_params = set(working.keys()) - allowed_params
|
| 1156 |
+
|
| 1157 |
if unknown_params and strict:
|
| 1158 |
raise ConstructorBindingError(
|
| 1159 |
f"Unknown constructor parameter(s) for {item_kind} at {path}: "
|
|
|
|
| 1167 |
}
|
| 1168 |
|
| 1169 |
missing_required = required_params - set(filtered.keys())
|
| 1170 |
+
|
| 1171 |
if missing_required:
|
| 1172 |
raise ConstructorBindingError(
|
| 1173 |
f"Missing required constructor parameter(s) for {item_kind} at {path}: "
|
|
|
|
| 1214 |
return allowed_params, required_params, accepts_var_keyword
|
| 1215 |
|
| 1216 |
@staticmethod
|
| 1217 |
+
def _behavior_runtime_id(
|
| 1218 |
+
spec: BehaviorSpec,
|
| 1219 |
+
*,
|
| 1220 |
+
owner_agent_id: str | None,
|
| 1221 |
+
behavior_index: int | None,
|
| 1222 |
+
) -> str:
|
| 1223 |
+
"""Return a deterministic runtime id for a behavior spec."""
|
| 1224 |
+
|
| 1225 |
+
metadata_id = None
|
| 1226 |
+
|
| 1227 |
+
if isinstance(spec.metadata, Mapping):
|
| 1228 |
+
metadata_id = spec.metadata.get("id") or spec.metadata.get("behavior_id")
|
| 1229 |
+
|
| 1230 |
+
if metadata_id is not None and str(metadata_id).strip():
|
| 1231 |
+
return str(metadata_id).strip()
|
| 1232 |
+
|
| 1233 |
+
owner = owner_agent_id or "world"
|
| 1234 |
+
index = 0 if behavior_index is None else int(behavior_index)
|
| 1235 |
+
return f"{owner}:behavior:{index}:{spec.name}"
|
| 1236 |
+
|
| 1237 |
+
@staticmethod
|
| 1238 |
+
def _agent_runtime_position(
|
| 1239 |
+
position: Any,
|
| 1240 |
+
*,
|
| 1241 |
+
dimensions: int | None = None,
|
| 1242 |
+
) -> np.ndarray:
|
| 1243 |
+
"""Return a NumPy position vector compatible with runtime agents."""
|
| 1244 |
+
|
| 1245 |
+
return WorldFactory._runtime_position(
|
| 1246 |
+
position,
|
| 1247 |
+
dimensions=dimensions,
|
| 1248 |
+
object_label="Agent",
|
| 1249 |
+
)
|
| 1250 |
|
| 1251 |
+
@staticmethod
|
| 1252 |
+
def _resource_runtime_position(
|
| 1253 |
+
position: Any,
|
| 1254 |
+
*,
|
| 1255 |
+
dimensions: int | None = None,
|
| 1256 |
+
) -> np.ndarray:
|
| 1257 |
+
"""Return a NumPy position vector compatible with runtime resources."""
|
| 1258 |
+
|
| 1259 |
+
return WorldFactory._runtime_position(
|
| 1260 |
+
position,
|
| 1261 |
+
dimensions=dimensions,
|
| 1262 |
+
object_label="Resource",
|
| 1263 |
+
)
|
| 1264 |
+
|
| 1265 |
+
@staticmethod
|
| 1266 |
+
def _runtime_position(
|
| 1267 |
+
position: Any,
|
| 1268 |
+
*,
|
| 1269 |
+
dimensions: int | None = None,
|
| 1270 |
+
object_label: str = "Object",
|
| 1271 |
+
) -> np.ndarray:
|
| 1272 |
+
"""Return a NumPy runtime position vector.
|
| 1273 |
+
|
| 1274 |
+
Runtime World implementations may validate positions with NumPy-style
|
| 1275 |
+
attributes such as ``position.size``. Therefore positions must not be
|
| 1276 |
+
tuples by the time objects are installed into the World.
|
| 1277 |
"""
|
| 1278 |
|
| 1279 |
+
target_dimensions = (
|
| 1280 |
+
int(dimensions)
|
| 1281 |
+
if dimensions is not None and int(dimensions) > 0
|
| 1282 |
+
else 2
|
| 1283 |
+
)
|
| 1284 |
+
|
| 1285 |
+
if position is None:
|
| 1286 |
+
return np.zeros(target_dimensions, dtype=np.float64)
|
| 1287 |
+
|
| 1288 |
+
try:
|
| 1289 |
+
array = np.asarray(position, dtype=np.float64).reshape(-1)
|
| 1290 |
+
except (TypeError, ValueError) as exc:
|
| 1291 |
+
raise RuntimeObjectBuildError(
|
| 1292 |
+
f"{object_label} position must be numeric, got {position!r}"
|
| 1293 |
+
) from exc
|
| 1294 |
+
|
| 1295 |
+
if array.size == 0:
|
| 1296 |
+
return np.zeros(target_dimensions, dtype=np.float64)
|
| 1297 |
+
|
| 1298 |
+
if not np.all(np.isfinite(array)):
|
| 1299 |
+
raise RuntimeObjectBuildError(
|
| 1300 |
+
f"{object_label} position must contain only finite values, got {position!r}"
|
| 1301 |
+
)
|
| 1302 |
+
|
| 1303 |
+
if int(array.size) != target_dimensions:
|
| 1304 |
+
raise RuntimeObjectBuildError(
|
| 1305 |
+
f"{object_label} position has {int(array.size)} dimension(s), "
|
| 1306 |
+
f"but world space expects {target_dimensions}"
|
| 1307 |
+
)
|
| 1308 |
+
|
| 1309 |
+
return array.astype(np.float64, copy=True)
|
| 1310 |
+
|
| 1311 |
+
def _ensure_agent_position_array(
|
| 1312 |
+
self,
|
| 1313 |
+
agent: Agent,
|
| 1314 |
+
*,
|
| 1315 |
+
dimensions: int | None = None,
|
| 1316 |
+
) -> None:
|
| 1317 |
+
"""Ensure agent.position is a NumPy array before adding it to World."""
|
| 1318 |
+
|
| 1319 |
+
raw_position = getattr(agent, "position", None)
|
| 1320 |
+
runtime_position = self._agent_runtime_position(
|
| 1321 |
+
raw_position,
|
| 1322 |
+
dimensions=dimensions,
|
| 1323 |
+
)
|
| 1324 |
+
self._force_setattr(agent, "position", runtime_position.copy())
|
| 1325 |
+
|
| 1326 |
+
repaired_position = getattr(agent, "position", None)
|
| 1327 |
+
if not hasattr(repaired_position, "size"):
|
| 1328 |
+
raise RuntimeObjectBuildError(
|
| 1329 |
+
"Agent.position could not be converted to a NumPy array before "
|
| 1330 |
+
f"world installation. Agent={getattr(agent, 'id', None)!r}, "
|
| 1331 |
+
f"position_type={type(repaired_position).__name__}"
|
| 1332 |
+
)
|
| 1333 |
+
|
| 1334 |
+
def _ensure_resource_position_array(
|
| 1335 |
+
self,
|
| 1336 |
+
resource: Resource,
|
| 1337 |
+
*,
|
| 1338 |
+
dimensions: int | None = None,
|
| 1339 |
+
) -> None:
|
| 1340 |
+
"""Ensure resource.position is a NumPy array before adding it to World."""
|
| 1341 |
+
|
| 1342 |
+
raw_position = getattr(resource, "position", None)
|
| 1343 |
+
runtime_position = self._resource_runtime_position(
|
| 1344 |
+
raw_position,
|
| 1345 |
+
dimensions=dimensions,
|
| 1346 |
+
)
|
| 1347 |
+
self._force_setattr(resource, "position", runtime_position.copy())
|
| 1348 |
+
|
| 1349 |
+
repaired_position = getattr(resource, "position", None)
|
| 1350 |
+
if not hasattr(repaired_position, "size"):
|
| 1351 |
+
raise RuntimeObjectBuildError(
|
| 1352 |
+
"Resource.position could not be converted to a NumPy array before "
|
| 1353 |
+
f"world installation. Resource={getattr(resource, 'id', None)!r}, "
|
| 1354 |
+
f"position_type={type(repaired_position).__name__}"
|
| 1355 |
+
)
|
| 1356 |
+
|
| 1357 |
+
@staticmethod
|
| 1358 |
+
def _safe_setattr(instance: Any, name: str, value: Any) -> bool:
|
| 1359 |
+
"""Set an attribute defensively."""
|
| 1360 |
+
|
| 1361 |
try:
|
| 1362 |
setattr(instance, name, value)
|
| 1363 |
return True
|
|
|
|
| 1370 |
)
|
| 1371 |
return False
|
| 1372 |
|
| 1373 |
+
@staticmethod
|
| 1374 |
+
def _force_setattr(instance: Any, name: str, value: Any) -> None:
|
| 1375 |
+
"""Set an attribute, using object.__setattr__ as a fallback."""
|
| 1376 |
+
|
| 1377 |
+
try:
|
| 1378 |
+
setattr(instance, name, value)
|
| 1379 |
+
return
|
| 1380 |
+
except Exception:
|
| 1381 |
+
pass
|
| 1382 |
+
|
| 1383 |
+
try:
|
| 1384 |
+
object.__setattr__(instance, name, value)
|
| 1385 |
+
return
|
| 1386 |
+
except Exception as exc:
|
| 1387 |
+
raise RuntimeObjectBuildError(
|
| 1388 |
+
f"Could not set attribute {name!r} on {instance.__class__.__name__}"
|
| 1389 |
+
) from exc
|
| 1390 |
+
|
| 1391 |
@staticmethod
|
| 1392 |
def _noop_behavior_factory(name: str) -> type[Behavior]:
|
| 1393 |
"""Create a no-op behavior class for permissive unknown behavior mode."""
|