-
Notifications
You must be signed in to change notification settings - Fork 10
feat: add external NAT reference adapter #175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rapids-bot
merged 5 commits into
NVIDIA:main
from
AjayThorve:feat/external-nat-reference-adapter
Aug 6, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3ea79cd
feat: add external NAT reference adapter
AjayThorve c6d8df0
fix: address NAT adapter review feedback
AjayThorve 9a271c2
refactor: adopt typed workflow configuration
AjayThorve 16c360c
refactor: make NAT calculator example self-contained
AjayThorve bce23f0
test: cover NAT adapter cleanup failures
AjayThorve File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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` | | ||
|
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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"]), | ||
|
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()) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.