Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions packages/nemo_platform/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ nemo-agents-plugin = [
"boto3>=1.40.46,<1.40.62",
"botocore>=1.40.46,<1.40.62",
"httpx>=0.27",
"nemo-fabric>=0.1.0a20260717,<0.2.0",
"nemo-fabric-runtime>=0.1.0a20260717,<0.2.0; sys_platform == 'linux'",
"pyyaml>=6.0",
"anthropic>=0.88.0",
"rich>=13.7.1",
Expand Down
9 changes: 6 additions & 3 deletions plugins/nemo-agents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ dependencies = [
"boto3>=1.40.46,<1.40.62",
"botocore>=1.40.46,<1.40.62",
"httpx>=0.27",
# TODO(AIRCORE-897): Move this to a stable Fabric version before release once available.
# TODO(AIRCORE-897): Add the `relay` extra once nemo-evaluator-sdk's nemo-relay pin allows >=0.5.
"nemo-fabric>=0.1.0a20260717,<0.2.0",
# Fabric runtime wheels are currently published for Linux only; macOS devs
# can build/install the native runtime from the NeMo-Fabric repo if needed.
"nemo-fabric-runtime>=0.1.0a20260717,<0.2.0; sys_platform == 'linux'",
"pyyaml>=6.0",
# TODO(AIRCORE-896): Add Fabric SDK/runtime and Relay as default
# dependencies once the PyPI wheels are available to the repo resolver.
# Harness adapter packages should stay target-environment dependencies.
# improvement/ subpackage — agent-improvement workflow (POC).
"anthropic>=0.88.0",
"rich>=13.7.1",
Expand Down
48 changes: 8 additions & 40 deletions plugins/nemo-agents/src/nemo_agents_plugin/fabric/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

from __future__ import annotations

import importlib
from typing import Any

import nemo_fabric as fabric
from nemo_agents_plugin.agent_config import AgentConfig, HarnessConfig, ModelConfig

HARNESS_ADAPTER_IDS = {
Expand All @@ -22,35 +22,22 @@ class FabricTranslationError(ValueError):
"""Raised when Platform agent config cannot be translated to Fabric config."""


def translate_agent_config(config: AgentConfig, harness_name: str | None = None) -> Any:
"""Translate Platform-owned agent config into a typed in-memory FabricConfig.

The Fabric SDK import is intentionally local to this function so existing
NAT-backed NeMo Agents paths do not require Fabric to be installed.
"""

(
FabricConfig,
HarnessConfig_,
MetadataConfig,
ModelConfig_,
EnvironmentConfig,
) = _fabric_model_types()

def translate_agent_config(config: AgentConfig, harness_name: str | None = None) -> fabric.FabricConfig:
"""Translate Platform-owned agent config into a typed in-memory FabricConfig."""
selected_harness_name, harness = _select_harness(config, harness_name)
model = _resolve_model(config, selected_harness_name, harness)

fabric_config = FabricConfig(
metadata=MetadataConfig(name=config.name, description=config.description or None),
harness=HarnessConfig_(
fabric_config = fabric.FabricConfig(
metadata=fabric.MetadataConfig(name=config.name, description=config.description or None),
harness=fabric.HarnessConfig(
adapter_id=_adapter_id_for_harness(harness),
resolution="preinstalled",
settings=harness.settings,
),
models={
"default": ModelConfig_(**_model_payload(model)),
"default": fabric.ModelConfig(**_model_payload(model)),
},
environment=EnvironmentConfig(
environment=fabric.EnvironmentConfig(
provider=config.environment.provider,
workspace=config.environment.workspace,
artifacts=config.environment.artifacts,
Expand All @@ -62,25 +49,6 @@ def translate_agent_config(config: AgentConfig, harness_name: str | None = None)
return fabric_config


def _fabric_model_types() -> tuple[type, type, type, type, type]:
# TODO(AIRCORE-896): Keep this import lazy until Fabric SDK/runtime wheels
# are available to the repo resolver and can be added as plugin dependencies.
try:
nemo_fabric = importlib.import_module("nemo_fabric")
except ImportError as error:
raise FabricTranslationError(
"NeMo Fabric SDK is required to translate nemo-agents-spec-v1 config to FabricConfig."
) from error

return (
getattr(nemo_fabric, "FabricConfig"),
getattr(nemo_fabric, "HarnessConfig"),
getattr(nemo_fabric, "MetadataConfig"),
getattr(nemo_fabric, "ModelConfig"),
getattr(nemo_fabric, "EnvironmentConfig"),
)


def _select_harness(config: AgentConfig, harness_name: str | None) -> tuple[str, HarnessConfig]:
selected_harness_name = harness_name or config.default_harness
harness = config.harnesses.get(selected_harness_name)
Expand Down
60 changes: 48 additions & 12 deletions plugins/nemo-agents/src/nemo_agents_plugin/fabric/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@
from __future__ import annotations

import asyncio
import importlib
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from nemo_agents_plugin.agent_config import AgentConfig
from nemo_agents_plugin.fabric.translator import FabricTranslationError, translate_agent_config
from nemo_fabric import Fabric, FabricConfigError
from pydantic import ValidationError

FABRIC_VALIDATION_TIMEOUT_SECONDS = 60.0


Expand All @@ -22,6 +27,16 @@ class FabricValidationResult:
doctor_report: Any


@dataclass(frozen=True, slots=True)
class PlatformFabricValidationResult:
"""Result of Platform config translation and Fabric validation."""

agent_config: AgentConfig
fabric_config: Any
Comment thread
mmogallapalli marked this conversation as resolved.
Outdated
plan: Any
doctor_report: Any
Comment thread
mmogallapalli marked this conversation as resolved.
Outdated


class FabricValidationError(ValueError):
"""Raised when Fabric planning or preflight validation fails."""

Expand All @@ -36,6 +51,30 @@ def __init__(self, status: str | None, failed_checks: list[str]) -> None:
super().__init__(f"Fabric preflight failed with status {status!r}: {details}")


async def validate_platform_agent_config(
config: AgentConfig | Mapping[str, Any],
*,
base_dir: Path | str,
harness_name: str | None = None,
fabric: Any | None = None,
) -> PlatformFabricValidationResult:
"""Translate and validate a Platform-owned agent config with Fabric."""

agent_config = _coerce_agent_config(config)
try:
fabric_config = translate_agent_config(agent_config, harness_name=harness_name)
except FabricTranslationError as error:
raise FabricValidationError(f"Fabric config translation failed: {error}") from error

validation_result = await validate_fabric_config(fabric_config, base_dir=base_dir, fabric=fabric)
return PlatformFabricValidationResult(
agent_config=agent_config,
fabric_config=fabric_config,
plan=validation_result.plan,
doctor_report=validation_result.doctor_report,
)


async def validate_fabric_config(
fabric_config: Any,
*,
Expand All @@ -45,11 +84,9 @@ async def validate_fabric_config(
"""Run Fabric plan and doctor for a translated FabricConfig.

This validates the selected harness and environment without invoking the
agent. The Fabric SDK import is intentionally local so NAT-backed paths do
not require Fabric to be installed.
agent. Fabric is a required dependency of the ``nemo-agents`` plugin.
"""

Fabric, FabricConfigError = _fabric_validation_types()
fabric_client = fabric or Fabric()

try:
Expand All @@ -71,15 +108,14 @@ async def validate_fabric_config(
return FabricValidationResult(plan=plan, doctor_report=doctor_report)


def _fabric_validation_types() -> tuple[type, type[Exception]]:
# TODO(AIRCORE-896): Keep this import lazy until Fabric SDK/runtime wheels
# are available to the repo resolver and can be added as plugin dependencies.
try:
nemo_fabric = importlib.import_module("nemo_fabric")
except ImportError as error:
raise FabricValidationError("NeMo Fabric SDK is required to plan and preflight FabricConfig.") from error
def _coerce_agent_config(config: AgentConfig | Mapping[str, Any]) -> AgentConfig:
if isinstance(config, AgentConfig):
return config

return getattr(nemo_fabric, "Fabric"), getattr(nemo_fabric, "FabricConfigError")
try:
return AgentConfig.model_validate(config)
except ValidationError as error:
raise FabricValidationError(f"Invalid Platform agent config: {error}") from error


def _ensure_doctor_passed(report: dict[str, Any]) -> None:
Expand Down
54 changes: 21 additions & 33 deletions plugins/nemo-agents/tests/unit/test_fabric_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
from __future__ import annotations

import copy
import importlib
import sys
import types
from types import SimpleNamespace
from typing import Any

import nemo_agents_plugin.fabric.translator as translator
import pytest
from nemo_agents_plugin.agent_config import AgentConfig
from nemo_agents_plugin.fabric.translator import FabricTranslationError, translate_agent_config
Expand Down Expand Up @@ -40,14 +39,18 @@ def enable_relay(


@pytest.fixture()
def fake_nemo_fabric(monkeypatch: pytest.MonkeyPatch) -> None:
module = types.ModuleType("nemo_fabric")
setattr(module, "EnvironmentConfig", _FabricObject)
setattr(module, "FabricConfig", _FakeFabricConfig)
setattr(module, "HarnessConfig", _FabricObject)
setattr(module, "MetadataConfig", _FabricObject)
setattr(module, "ModelConfig", _FabricObject)
monkeypatch.setitem(sys.modules, "nemo_fabric", module)
def fake_fabric_models(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
translator,
"fabric",
SimpleNamespace(
EnvironmentConfig=_FabricObject,
FabricConfig=_FakeFabricConfig,
HarnessConfig=_FabricObject,
MetadataConfig=_FabricObject,
ModelConfig=_FabricObject,
),
)
Comment thread
mmogallapalli marked this conversation as resolved.
Outdated


def _example_yaml_config() -> dict[str, Any]:
Expand Down Expand Up @@ -107,7 +110,7 @@ def _example_yaml_config() -> dict[str, Any]:


class TestTranslateAgentConfig:
def test_translates_default_harness(self, fake_nemo_fabric: None) -> None:
def test_translates_default_harness(self, fake_fabric_models: None) -> None:
config = AgentConfig.model_validate(_example_yaml_config())

fabric_config = translate_agent_config(config)
Expand All @@ -124,7 +127,7 @@ def test_translates_default_harness(self, fake_nemo_fabric: None) -> None:
assert fabric_config.environment.artifacts == "./artifacts"
assert not hasattr(fabric_config, "relay")

def test_selected_harness_uses_default_model(self, fake_nemo_fabric: None) -> None:
def test_selected_harness_uses_default_model(self, fake_fabric_models: None) -> None:
config = AgentConfig.model_validate(_example_yaml_config())

fabric_config = translate_agent_config(config, harness_name="codex")
Expand All @@ -145,7 +148,7 @@ def test_selected_harness_uses_default_model(self, fake_nemo_fabric: None) -> No
)
def test_supported_harness_kinds_translate_to_adapter_ids(
self,
fake_nemo_fabric: None,
fake_fabric_models: None,
kind: str,
adapter_id: str,
) -> None:
Expand All @@ -158,13 +161,13 @@ def test_supported_harness_kinds_translate_to_adapter_ids(

assert fabric_config.harness.adapter_id == adapter_id

def test_unknown_selected_harness_rejected(self, fake_nemo_fabric: None) -> None:
def test_unknown_selected_harness_rejected(self, fake_fabric_models: None) -> None:
config = AgentConfig.model_validate(_example_yaml_config())

with pytest.raises(FabricTranslationError, match="Unknown configured harness 'claude'"):
translate_agent_config(config, harness_name="claude")

def test_unsupported_harness_kind_rejected(self, fake_nemo_fabric: None) -> None:
def test_unsupported_harness_kind_rejected(self, fake_fabric_models: None) -> None:
payload = _example_yaml_config()
payload["harnesses"]["custom"] = {"kind": "custom"}
payload["default_harness"] = "custom"
Expand All @@ -173,7 +176,7 @@ def test_unsupported_harness_kind_rejected(self, fake_nemo_fabric: None) -> None
with pytest.raises(FabricTranslationError, match="Unsupported harness kind 'custom'"):
translate_agent_config(config)

def test_missing_model_rejected(self, fake_nemo_fabric: None) -> None:
def test_missing_model_rejected(self, fake_fabric_models: None) -> None:
payload = _example_yaml_config()
payload["models"] = {}
payload["default_harness"] = "codex"
Expand All @@ -182,7 +185,7 @@ def test_missing_model_rejected(self, fake_nemo_fabric: None) -> None:
with pytest.raises(FabricTranslationError, match="no models.default is configured"):
translate_agent_config(config)

def test_relay_telemetry_uses_latest_fabric_shape(self, fake_nemo_fabric: None) -> None:
def test_relay_telemetry_uses_latest_fabric_shape(self, fake_fabric_models: None) -> None:
payload = copy.deepcopy(_example_yaml_config())
payload["telemetry"]["enabled"] = True
config = AgentConfig.model_validate(payload)
Expand All @@ -208,18 +211,3 @@ def test_relay_telemetry_uses_latest_fabric_shape(self, fake_nemo_fabric: None)
"output_directory": "./artifacts/relay",
},
}

def test_missing_fabric_dependency_reports_actionable_error(self, monkeypatch: pytest.MonkeyPatch) -> None:
real_import_module = importlib.import_module

def fake_import_module(name: str, package: str | None = None) -> Any:
if name == "nemo_fabric":
raise ImportError("No module named 'nemo_fabric'")
return real_import_module(name, package)

monkeypatch.delitem(sys.modules, "nemo_fabric", raising=False)
monkeypatch.setattr(importlib, "import_module", fake_import_module)
config = AgentConfig.model_validate(_example_yaml_config())

with pytest.raises(FabricTranslationError, match="NeMo Fabric SDK is required"):
translate_agent_config(config)
Loading
Loading