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
21 changes: 21 additions & 0 deletions external/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# External Adapter References

This directory contains source-only reference adapters that exercise the public
NVIDIA NeMo Fabric adapter contract without becoming bundled NeMo Fabric
adapters. Each reference owns its descriptor, implementation, examples, and
documentation.

These adapters are not published as wheels and are not wired into bundled or
installed-adapter discovery. Packaging and discovery are separate concerns from
the adapter contract demonstrated here.

The following source-only reference adapter is available:

| Harness | Adapter ID | Reference |
| --- | --- | --- |
| NVIDIA NeMo Agent Toolkit | `nvidia.fabric.nat` | [NAT adapter](nat/README.md) |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
118 changes: 118 additions & 0 deletions external/nat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# NVIDIA NeMo Fabric NAT Reference Adapter

This source-only adapter runs an NVIDIA NeMo Agent Toolkit (NAT) workflow behind
the NeMo Fabric lifecycle contract. It is a third-party adapter reference, not a
bundled NeMo Fabric adapter or a published package.

The implementation is generic. It constructs NAT configuration in memory from
`FabricConfig`; it does not read a NAT YAML file and does not hardcode the
calculator or email-phishing components.

## Configuration Boundary

NeMo Fabric owns portable configuration. `workflow` selects and configures the
NAT executable, while `harness.settings` contains NAT-native function components
that have no portable NeMo Fabric equivalent.

| NeMo Fabric input | NAT configuration |
| --- | --- |
| `models.<role>` | `llms.<role>`; every NeMo Fabric model-role name is preserved |
| `instructions.system` | Built-in `react_agent` workflow `additional_instructions`; other workflow types reject this field in the initial adapter |
| `workflow.entrypoint.kind=nat_workflow` | Resolve a registered NAT workflow component |
| `workflow.entrypoint.ref` | `workflow._type` |
| `workflow.settings` | Remaining `workflow` component fields |
| `harness.settings.functions` | `functions` |
| `harness.settings.function_groups` | `function_groups` |
Comment thread
AjayThorve marked this conversation as resolved.
| Harness-native `mcp.servers.<name>` | Generated `mcp_client` function group named `<name>` |
| `tools.enabled`, `tools.blocked` | Effective NAT-native workflow tool selection |

The adapter loads installed `nat.components` entry points before NAT validates
the generated configuration. A custom function, function group, or workflow is
therefore supplied as an installed NAT component package and selected by its
registered type in `workflow.entrypoint.ref` or the component `_type` in
`harness.settings`. No Python import path or callable crosses `FabricConfig`.

At runtime, `start` loads components, enters one `WorkflowBuilder`, creates a
`SessionManager` with that shared builder, and retains both resources. Each
`invoke` opens a session from the retained manager, enters `session.run(...)`,
and awaits `runner.result()`. `stop` shuts down the session manager and exits
the builder context. This first reference does not claim cancellation, service,
streaming, or live-update support.

## MCP Tool Filters

The adapter consumes the routed `capability_plan.native.mcp_servers` entries,
including the normalized per-server filters. NeMo Fabric MCP tool names remain bare
server-local names; NAT exposes a selected member as `<server>__<tool>`.

| NeMo Fabric server policy | Generated NAT function group |
| --- | --- |
| `allowed_tools` omitted and `blocked_tools=[]` | No `include` or `exclude`; expose all discovered tools |
| Nonempty `allowed_tools` only | `include=allowed_tools` |
| `allowed_tools` omitted and nonempty `blocked_tools` | `exclude=blocked_tools` |
| Both lists configured | `include=allowed_tools`; NeMo Fabric requires `blocked_tools` to be disjoint, so those names are already outside the allowlist |
| `allowed_tools=[]` | Omit the generated group; expose no tools from that server |

NAT rejects a function group that sets both `include` and `exclude`, so the
adapter emits only `include` whenever an allowlist is present. NeMo Fabric rejects
blank names and an allow/block overlap before adapter startup. A nonempty
generated MCP group is added to workflows that expose `tool_names`; callers do
not repeat portable MCP servers in `harness.settings`. A workflow implementation
that requires at least one tool can still reject a configuration whose effective
tool set is empty.

Per-server MCP filters and root `tools.enabled` or `tools.blocked` solve
different problems. MCP filters select members within one server. Root tool
policy selects across the effective NAT-native tool surface.

## Development Bootstrap

This directory intentionally has no package metadata or discovery wiring. Until
source resolution and third-party descriptor discovery are available, use one
Python environment for NeMo Fabric, the common adapter host, NAT, and every NAT
component referenced by the config:

```bash
uv pip install \
nemo-fabric-adapters-common \
nvidia-nat-core \
nvidia-nat-langchain \
nvidia-nat-mcp
export PYTHONPATH="$PWD/external/nat/src${PYTHONPATH:+:$PYTHONPATH}"
```

`PYTHONPATH` is a development bootstrap limitation, not the target installation
contract. Stage the descriptor in the current agent-local discovery location:

```bash
mkdir -p .tmp/nat-reference/adapters/nat
cp external/nat/fabric-adapter.json \
.tmp/nat-reference/adapters/nat/fabric-adapter.json
```

The calculator example starts its source-only MCP server over stdio, so it does
not require a separately managed endpoint. Run the typed `FabricConfig` example:

```bash
uv run python external/nat/examples/calculator.py \
--base-dir "$PWD/.tmp/nat-reference"
```

The email-phishing example uses the NAT example component registered by
`nat_email_phishing_analyzer`. Install that component from a NAT checkout, then
run the example:

```bash
uv pip install -e \
"<path-to-nat-checkout>/examples/evaluation_and_profiling/email_phishing_analyzer"
uv run python external/nat/examples/email_phishing.py \
--base-dir "$PWD/.tmp/nat-reference"
```

Both examples accept `--plan` to inspect the resolved plan without starting the
runtime. They use Python `FabricConfig` objects only; no YAML is involved.
90 changes: 90 additions & 0 deletions external/nat/examples/calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Run a NAT ReAct workflow with a portable calculator MCP server."""

from __future__ import annotations

import argparse
import asyncio
import json
import shlex
import sys
from pathlib import Path

from nemo_fabric import Fabric
from nemo_fabric import FabricConfig
from nemo_fabric import HarnessConfig
from nemo_fabric import InstructionConfig
from nemo_fabric import InstructionsConfig
from nemo_fabric import MetadataConfig
from nemo_fabric import ModelConfig
from nemo_fabric import RuntimeConfig
from nemo_fabric import WorkflowConfig
from nemo_fabric import WorkflowEntrypointConfig


def build_config() -> FabricConfig:
"""Build the portable calculator configuration."""

config = FabricConfig(
metadata=MetadataConfig(
name="nat-calculator",
description="Uses calculator tools exposed by an MCP server.",
),
harness=HarnessConfig(
adapter_id="nvidia.fabric.nat",
resolution="preinstalled",
),
workflow=WorkflowConfig(
entrypoint=WorkflowEntrypointConfig(
kind="nat_workflow",
ref="react_agent",
),
settings={"llm_name": "default"},
),
models={
"default": ModelConfig(
provider="nvidia",
model="nvidia/nemotron-3-nano-30b-a3b",
api_key_env="NVIDIA_API_KEY",
temperature=0.0,
)
},
instructions=InstructionsConfig(
system=InstructionConfig(
content="Use the calculator tools for arithmetic. Return a concise answer."
)
),
runtime=RuntimeConfig(input_schema="text", output_schema="message"),
)
server = Path(__file__).with_name("calculator_mcp.py")
config.add_mcp_server(
"calculator",
transport="stdio",
url=shlex.join([sys.executable, str(server)]),
exposure="harness_native",
blocked_tools=["divide"],
)
return config


async def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-dir", type=Path, default=Path.cwd())
parser.add_argument("--input", default="What is 21 multiplied by 2?")
parser.add_argument("--plan", action="store_true")
args = parser.parse_args()

fabric = Fabric()
config = build_config()
output = (
fabric.plan(config, base_dir=args.base_dir)
if args.plan
else await fabric.run(config, base_dir=args.base_dir, input=args.input)
)
print(json.dumps(output.to_mapping(), indent=2))


if __name__ == "__main__":
asyncio.run(main())
44 changes: 44 additions & 0 deletions external/nat/examples/calculator_mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Self-contained calculator MCP server for the NAT reference example."""

from __future__ import annotations

from mcp.server.fastmcp import FastMCP

server = FastMCP("calculator")


@server.tool()
def add(left: float, right: float) -> float:
"""Add two numbers."""

return left + right


@server.tool()
def subtract(left: float, right: float) -> float:
"""Subtract the right value from the left value."""

return left - right


@server.tool()
def multiply(left: float, right: float) -> float:
"""Multiply two numbers."""

return left * right


@server.tool()
def divide(left: float, right: float) -> float:
"""Divide the left value by the right value."""

if right == 0:
raise ValueError("cannot divide by zero")
return left / right


if __name__ == "__main__":
server.run(transport="stdio")
95 changes: 95 additions & 0 deletions external/nat/examples/email_phishing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

"""Run a NAT workflow with the installed email-phishing analyzer function."""

from __future__ import annotations

import argparse
import asyncio
import json
from pathlib import Path

from nemo_fabric import Fabric
from nemo_fabric import FabricConfig
from nemo_fabric import HarnessConfig
from nemo_fabric import InstructionConfig
from nemo_fabric import InstructionsConfig
from nemo_fabric import MetadataConfig
from nemo_fabric import ModelConfig
from nemo_fabric import RuntimeConfig
from nemo_fabric import ToolsConfig
from nemo_fabric import WorkflowConfig
from nemo_fabric import WorkflowEntrypointConfig


def build_config() -> FabricConfig:
"""Build the NAT-native email-phishing configuration."""

return FabricConfig(
metadata=MetadataConfig(
name="nat-email-phishing-analyzer",
description="Classifies an email with an installed NAT function.",
),
harness=HarnessConfig(
adapter_id="nvidia.fabric.nat",
resolution="preinstalled",
settings={
"functions": {
"email_phishing_analyzer": {
"_type": "email_phishing_analyzer",
"llm": "default",
}
}
},
),
workflow=WorkflowConfig(
entrypoint=WorkflowEntrypointConfig(
kind="nat_workflow",
ref="react_agent",
),
settings={
"llm_name": "default",
"use_native_tool_calling": True,
},
),
models={
"default": ModelConfig(
provider="nvidia",
model="nvidia/nemotron-3-nano-30b-a3b",
api_key_env="NVIDIA_API_KEY",
temperature=0.0,
)
},
instructions=InstructionsConfig(
system=InstructionConfig(
content='State whether the email is "phishing" or "benign" and explain why.'
)
),
tools=ToolsConfig(enabled=["email_phishing_analyzer"]),
Comment thread
AjayThorve marked this conversation as resolved.
runtime=RuntimeConfig(input_schema="text", output_schema="message"),
)


async def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--base-dir", type=Path, default=Path.cwd())
parser.add_argument(
"--input",
default="Urgent: confirm your password at http://example.invalid today.",
)
parser.add_argument("--plan", action="store_true")
args = parser.parse_args()

fabric = Fabric()
config = build_config()
output = (
fabric.plan(config, base_dir=args.base_dir)
if args.plan
else await fabric.run(config, base_dir=args.base_dir, input=args.input)
)
print(json.dumps(output.to_mapping(), indent=2))


if __name__ == "__main__":
asyncio.run(main())
Loading
Loading