Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 0 additions & 42 deletions src/physicalai/inference/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,48 +344,6 @@ def load(cls, path: str | Path) -> Manifest:
with p.open(encoding="utf-8") as fh:
return cls.model_validate(json.load(fh))

@classmethod
def from_legacy_metadata(cls, metadata: dict[str, Any]) -> Manifest:
"""Upgrade a legacy ``metadata.yaml`` dict to a ``Manifest``.

Maps flat keys (``policy_class``, ``backend``, ``use_action_queue``,
``chunk_size``) into the structured manifest schema so that
downstream code can use a single Manifest type regardless of the
on-disk format.

Args:
metadata: Dict loaded from ``metadata.yaml`` or ``metadata.json``.

Returns:
A ``Manifest`` populated with as much information as the legacy
format provides.
"""
policy_class = metadata.get("policy_class", "")
policy_name = _policy_name_from_class_path(policy_class)

from physicalai.inference.runners.single_pass import SinglePass # noqa: PLC0415

runner = ComponentSpec.from_class(SinglePass)

backend = metadata.get("backend", "")
artifacts: dict[str, str] = {backend: ""} if backend else {}

return cls.model_validate({
"policy": {
"name": policy_name,
"source": {"class_path": policy_class},
},
"model": {
"runner": {"class_path": runner.class_path, "init_args": runner.init_args},
"artifacts": artifacts,
},
**{
k: v
for k, v in metadata.items()
if k not in {"policy_class", "backend", "use_action_queue", "chunk_size"}
},
})

def save(self, path: str | Path) -> None:
"""Write the manifest as ``manifest.json``.

Expand Down
40 changes: 5 additions & 35 deletions src/physicalai/inference/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@

from __future__ import annotations

import json
import warnings
from collections import deque
from pathlib import Path
from typing import TYPE_CHECKING, Any, Self

import numpy as np
import yaml

from physicalai.inference.adapters import adapter_registry, get_adapter
from physicalai.inference.component_factory import instantiate_component, resolve_artifact
Expand Down Expand Up @@ -72,7 +69,7 @@ def __init__(
Args:
export_dir: Directory containing exported policy files
policy_name: Policy name (auto-detected if None)
backend: Backend to use, or 'auto' to detect from metadata/files
backend: Backend to use, or 'auto' to detect from manifest/files
device: Device for inference ('auto', 'cpu', 'cuda', 'CPU', 'GPU', etc.)
runner: Execution runner override. If None, auto-selected from manifest.
preprocessors: Pipeline stages applied to observations before the
Expand Down Expand Up @@ -318,38 +315,15 @@ def _prepare_inputs(self, inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray
return inputs

def _load_manifest(self) -> Manifest:
"""Load export manifest from manifest.json, metadata.yaml, or metadata.json.

Tries ``manifest.json`` first (new format), then falls back to
``metadata.yaml`` and ``metadata.json`` for backward compatibility.
"""Load export manifest from ``manifest.json``.

Returns:
Parsed Manifest instance.
Parsed Manifest instance, or an empty Manifest if no
``manifest.json`` exists in the export directory.
Comment thread
sovrasov marked this conversation as resolved.
"""
manifest_path = self.export_dir / "manifest.json"
if manifest_path.exists():
return Manifest.load(manifest_path)

yaml_path = self.export_dir / "metadata.yaml"
json_path = self.export_dir / "metadata.json"
legacy_path = yaml_path if yaml_path.exists() else json_path if json_path.exists() else None

if legacy_path is not None:
warnings.warn(
f"Loading from '{legacy_path.name}' is deprecated. "
"Re-export your model to generate 'manifest.json'. "
"Legacy metadata support will be removed in a future release.",
DeprecationWarning,
stacklevel=2,
)
if legacy_path.suffix == ".yaml":
with legacy_path.open(encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
else:
with legacy_path.open(encoding="utf-8") as f:
raw = json.load(f)
return Manifest.from_legacy_metadata(raw)

return Manifest()

def _load_processors(self, specs: list[ComponentSpec]) -> list[Any]:
Expand Down Expand Up @@ -399,7 +373,7 @@ def _detect_policy_name(self) -> str:
raise ValueError(msg)

def _detect_backend_from_manifest(self) -> str | None:
"""Extract backend from manifest artifacts or legacy extra data.
"""Extract backend from manifest artifacts.

Returns:
Backend string, or ``None`` if not found.
Expand All @@ -408,10 +382,6 @@ def _detect_backend_from_manifest(self) -> str | None:
if artifacts:
return next(iter(artifacts))

legacy_backend = (self.manifest.model_extra or {}).get("backend")
if legacy_backend:
return str(legacy_backend)

return None

def _detect_backend(self) -> str:
Expand Down
21 changes: 11 additions & 10 deletions src/physicalai/inference/runners/factory.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Runner factory for selecting inference runners from manifest or metadata."""
"""Runner factory for selecting inference runners from a manifest."""

from __future__ import annotations

Expand All @@ -15,18 +15,19 @@


def get_runner(source: Manifest | dict[str, Any]) -> InferenceRunner:
"""Select and instantiate a runner from a manifest or legacy metadata.
"""Select and instantiate a runner from a manifest.

Supports three formats:
Supports three input formats:

1. **Manifest object** — reads ``source.model.runner`` and
instantiates via :func:`instantiate_component`.
2. **Dict with runner spec** — raw manifest dict containing a
``"model"`` section with a runner component spec.
3. **Legacy dict** — falls back to a plain ``SinglePass`` runner.
runner component spec either under ``"model" -> "runner"``
or at the top-level ``"runner"`` key.
3. **Dict without runner spec** — falls back to a plain ``SinglePass`` runner.

Args:
source: A :class:`Manifest` instance or a raw metadata dict.
source: A :class:`Manifest` instance or a raw manifest dict.

Returns:
Configured runner instance.
Expand Down Expand Up @@ -61,19 +62,19 @@ def get_runner(source: Manifest | dict[str, Any]) -> InferenceRunner:
return SinglePass()


def _extract_runner_spec(metadata: dict[str, Any]) -> dict[str, Any] | None:
"""Extract a runner spec dict from nested or flat metadata.
def _extract_runner_spec(manifest_dict: dict[str, Any]) -> dict[str, Any] | None:
"""Extract a runner spec dict from nested or flat manifest data.

Returns:
Runner spec dict if found, otherwise ``None``.
"""
model_section = metadata.get("model", {})
model_section = manifest_dict.get("model", {})
if isinstance(model_section, dict):
runner = model_section.get("runner")
if isinstance(runner, dict) and ("class_path" in runner or "type" in runner):
return runner

runner = metadata.get("runner")
runner = manifest_dict.get("runner")
if isinstance(runner, dict) and ("class_path" in runner or "type" in runner):
return runner

Expand Down
22 changes: 14 additions & 8 deletions tests/unit/inference/test_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,25 @@ def mock_adapter() -> MagicMock:

@pytest.fixture
def mock_export_dir(tmp_path: Path) -> Path:
import yaml
import json

export_dir = tmp_path / "exports"
export_dir.mkdir()

metadata = {
"policy_class": "physicalai.policies.act.ACT",
"backend": "openvino",
"use_action_queue": False,
"chunk_size": 1,
manifest = {
"format": "policy_package",
"version": "1.0",
"policy": {
"name": "act",
"source": {"class_path": "physicalai.policies.act.ACT"},
},
"model": {
"artifacts": {"openvino": "act.xml"},
"runner": {"class_path": "physicalai.inference.runners.SinglePass", "init_args": {}},
},
}
with (export_dir / "metadata.yaml").open("w") as f:
yaml.dump(metadata, f)
with (export_dir / "manifest.json").open("w") as f:
json.dump(manifest, f)

(export_dir / "act.xml").touch()
(export_dir / "act.bin").touch()
Expand Down
30 changes: 0 additions & 30 deletions tests/unit/inference/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,36 +422,6 @@ def test_load_directory_without_manifest(self, tmp_path: Path) -> None:
Manifest.load(tmp_path)


class TestManifestFromLegacyMetadata:
def test_single_pass_policy(self) -> None:
metadata = {
"policy_class": "physicalai.policies.act.policy.ACT",
"backend": "openvino",
"use_action_queue": False,
"chunk_size": 1,
}
manifest = Manifest.from_legacy_metadata(metadata)

assert manifest.policy.name == "policy"
assert manifest.policy.source.class_path == "physicalai.policies.act.policy.ACT"
assert manifest.model.runner is not None
assert "SinglePass" in manifest.model.runner.class_path

def test_legacy_extra_preserved(self) -> None:
metadata = {
"policy_class": "test.Policy",
"backend": "openvino",
"physicalai_train_version": "1.2.3",
}
manifest = Manifest.from_legacy_metadata(metadata)
assert manifest.model_extra is not None
assert manifest.model_extra["physicalai_train_version"] == "1.2.3"

def test_empty_metadata(self) -> None:
manifest = Manifest.from_legacy_metadata({})
assert manifest.model.runner is not None


class TestManifestSerialization:
def test_roundtrip(self, tmp_path: Path) -> None:
original = Manifest(
Expand Down
Loading
Loading