Skip to content
Draft
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
34 changes: 34 additions & 0 deletions plugins/nemo-agents/examples/nat-to-fabric-spike/ANALYSIS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# NAT agent analysis: research_orchestrator

Migrated from a NAT reasoning_agent wrapping react_agent 'research_orchestrator'.

## Composition

- **research_orchestrator** (react_agent, main agent)
- tool: code_generation
- **math_agent** (tool_calling_agent, sub-agent)
- tool: mcp_math
- **time_agent** (tool_calling_agent, sub-agent)
- tool: mcp_time
- tool: current_timezone
- **jira_agent** (tool_calling_agent, sub-agent)
- tool: mcp_jira

## Models

- `default`: nvidia / nvidia/llama-3.3-nemotron-super-49b-v1
- `worker_llm`: nvidia / nvidia/nemotron-3-nano-30b-a3b

## Tools

MCP servers:
- `mcp_math` (streamable-http)
- `mcp_time` (stdio)
- `mcp_jira` (streamable-http)
NAT builtins (would need an MCP equivalent to run under Deep Agents):
- `code_generation`
- `current_timezone`

## Summary

4 agent(s), 3 MCP server(s), 2 NAT builtin(s), 0 feature(s) needing another home, 0 unresolved item(s).
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# NAT -> Fabric migration report

**Status: ready after 4 manual step(s)**

## MCP servers carried across
- mcp_math: streamable-http, url via env var
- mcp_time: stdio, no credentials
- mcp_jira: streamable-http, url via env var

## Environment variables to set before running
- `CORPORATE_MCP_JIRA_URL`
- `MATH_MCP_URL`
- `NAT_REDIRECT_URI`

## Auth requiring action (Fabric adapter gap)
- mcp_jira: NAT used auth_provider 'mcp_oauth2_jira' (_type mcp_oauth2). Deep Agents carries only ${ENV} URLs, so this needs a token-in-URL gateway or Fabric adapter OAuth2 support.

## Builtin tools requiring an MCP equivalent
- `code_generation`: NAT in-process tool. Needs a prebuilt MCP server equivalent before it runs under Deep Agents.
- `current_timezone`: NAT in-process tool. Needs a prebuilt MCP server equivalent before it runs under Deep Agents.

## Errors (must resolve)
- None.

## Features not carried (need a Fabric, Platform, or Relay home)
- None.

## Notes
- Unwrapped reasoning_agent onto 'research_orchestrator' as the main Deep Agent.
- Main agent 'react_agent' had no explicit system_prompt; its default lives in NAT Python and must be resolved via WorkflowBuilder.
- Main-agent direct tools (not sub-agents): code_generation.
69 changes: 69 additions & 0 deletions plugins/nemo-agents/examples/nat-to-fabric-spike/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# NAT to Fabric transpile spike

Stage 1 proof that an existing NAT (NVIDIA NeMo Agent Toolkit) agent can be migrated to a Fabric-native agent by translation, not by a permanent NAT compatibility shim. It reads a NAT workflow config and emits a Fabric `agent.yaml` for the LangChain Deep Agents harness, plus a migration report that carries MCP servers across and flags what needs auth.

## Why

A NAT agent can be moved to Fabric by translating it into a Fabric-native agent, rather than running its config through a permanent compatibility shim. This spike shows that path end to end for a non-trivial agent, and it makes the boundaries visible, including the tools that need more than a config rewrite.

## What it does

Input: `nat_agent/config.yml`, a deliberately non-trivial NAT topology.

```
reasoning_agent (workflow)
└─ research_orchestrator (react_agent)
├─ math_agent (tool_calling_agent) -> mcp_math (streamable-http, ${ENV} url)
├─ time_agent (tool_calling_agent) -> mcp_time (stdio) + current_timezone
├─ jira_agent (tool_calling_agent) -> mcp_jira (streamable-http, OAuth2)
└─ code_generation (builtin)
```

Output: `fabric/agent.yaml` (Deep Agents harness) and `MIGRATION_REPORT.md`.

The transpiler does four things:

1. Walks the NAT composition graph. Sub-agents used as tools become Deep Agents subagents, so the topology survives instead of flattening to one agent. The `reasoning_agent` wrapper is unwrapped onto the executing agent.
2. Maps each NAT LLM to a Fabric model. The main agent's model becomes `models.default`; the rest are emitted as aliases.
3. Carries MCP servers across. NAT `mcp_client` function groups map one-to-one to Fabric `mcp.servers`. stdio becomes a command URL; streamable-http keeps its URL and any `${ENV}` reference.
4. Reports instead of guessing. Env-var URLs are listed for the user to set. NAT builtins are flagged as needing a prebuilt MCP equivalent. Auth that Fabric's adapter cannot carry today is called out.

## Result

Running the transpiler on the fixture produces a config that passes the Fabric contract check, carries all three MCP servers, preserves all three sub-agents, and flags one auth gap. See `MIGRATION_REPORT.md` for the generated findings.

Two findings worth reading before anyone promises "seamless":

- **Auth gap.** Fabric's Deep Agents adapter forwards only `transport` and `url` per MCP server. A `${ENV}` in the URL is the one credential path that reaches the server. NAT servers using OAuth2 providers or custom headers (the `mcp_jira` case here) cannot carry as-is. Closing that is Fabric adapter work.
- **Builtins.** NAT builtin tools (`current_timezone`, `code_generation`) are in-process Python, not MCP servers. Each needs a prebuilt MCP equivalent before it runs under Deep Agents.

## Scope and honesty

This is Stage 1. It does not run the migrated agent against a live Fabric runtime; that is Stage 2 and needs a Fabric environment plus an API key.

The transpiler reads the NAT YAML directly so it runs with only PyYAML. A production version resolves the config through NAT's `WorkflowBuilder.from_config()` to also recover default prompts (which live in NAT's Python, not the YAML) and each tool's resolved schema. The one place this shows up is the main agent's `system_prompt`, emitted here as a `[RESOLVE]` marker.

Validation is a self-contained structural check of the Fabric contract. For full JSON Schema validation, point `--schema` at a local Fabric checkout:

```bash
python3 transpile.py --schema /path/to/nemo-fabric/schemas/agent.schema.json
```

## Analyze mode

The same introspection also runs read-only. `--analyze` skips the Fabric emission and writes `ANALYSIS.md`: the agent's composition tree, models, tools, and anything that would need work to move. It's the low-friction on-ramp: install NeMo Platform, hand it a NAT workflow, and it tells you what your agent does and how it's composed, with no migration commitment.

```bash
python3 transpile.py --analyze # writes ANALYSIS.md
python3 transpile.py --in path/to/agent.yml --analyze
```

The migration report and the analysis both call out NAT features this transpiler does not carry (a top-level `middleware:` section such as NASSE maps to NeMo Relay; `memory`, `retrievers`, and similar need a Fabric or Platform equivalent). Those aren't dropped silently; they're listed as work that needs a home.

## Run

```bash
pip install pyyaml # jsonschema optional, only for --schema
python3 transpile.py # reads nat_agent/config.yml, writes fabric/agent.yaml + MIGRATION_REPORT.md
python3 transpile.py --analyze # read-only analysis -> ANALYSIS.md
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Make the spike's transpile module importable when tests run from any cwd."""
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Generated by transpile.py from nat_agent/config.yml. Do not edit by hand.
# schema_version: fabric.agent/v1alpha1 (NeMo Fabric).
schema_version: fabric.agent/v1alpha1
metadata:
name: research_orchestrator
description: Migrated from a NAT reasoning_agent wrapping react_agent 'research_orchestrator'.
harness:
adapter_id: nvidia.fabric.langchain.deepagents
resolution: preinstalled
settings:
system_prompt: '[RESOLVE] Default react_agent prompt (recover via NAT WorkflowBuilder).'
deepagents:
subagents:
- name: math_agent
description: Handles arithmetic and calculation requests.
tools:
- mcp_math
- name: time_agent
description: Answers date and time questions. Resolve the timezone before
asking for the time.
tools:
- mcp_time
- current_timezone
- name: jira_agent
description: Looks up and summarizes Jira issues.
tools:
- mcp_jira
models:
default:
provider: nvidia
model: nvidia/llama-3.3-nemotron-super-49b-v1
temperature: 0.0
api_key_env: NVIDIA_API_KEY
settings:
thinking: true
max_tokens: 2000
worker_llm:
provider: nvidia
model: nvidia/nemotron-3-nano-30b-a3b
temperature: 0.0
api_key_env: NVIDIA_API_KEY
settings:
max_tokens: 1024
runtime:
input_schema: chat
output_schema: message
mcp:
servers:
mcp_math:
transport: streamable-http
url: ${MATH_MCP_URL}
exposure: harness_native
mcp_time:
transport: stdio
url: python -m mcp_server_time --local-timezone=America/Los_Angeles
exposure: harness_native
mcp_jira:
transport: streamable-http
url: ${CORPORATE_MCP_JIRA_URL}
exposure: harness_native
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# NAT (NVIDIA NeMo Agent Toolkit) source agent for the NAT-to-Fabric transpile spike.
#
# Topology (deliberately non-trivial):
#
# reasoning_agent (workflow entry)
# └─ augmented_fn -> research_orchestrator (react_agent)
# ├─ math_agent (tool_calling_agent) -> mcp_math (streamable-http, ${ENV} url)
# ├─ time_agent (tool_calling_agent) -> mcp_time (stdio) + current_timezone (builtin)
# ├─ jira_agent (tool_calling_agent) -> mcp_jira (streamable-http, OAuth2 auth)
# └─ code_generation (builtin)
#
# This exercises: a reasoning wrapper, an orchestrator, three sub-agents used as
# tools, two builtin tools, and three MCP servers across all three auth cases
# (none, env-var url, OAuth2). The OAuth2 case is the one Fabric's Deep Agents
# adapter cannot carry today, so the transpiler flags it instead of dropping it.

llms:
orchestrator_llm:
_type: nim
model_name: nvidia/llama-3.3-nemotron-super-49b-v1
thinking: true
temperature: 0.0
max_tokens: 2000
worker_llm:
_type: nim
model_name: nvidia/nemotron-3-nano-30b-a3b
temperature: 0.0
max_tokens: 1024

function_groups:
mcp_time:
_type: mcp_client
server:
transport: stdio
command: python
args: ["-m", "mcp_server_time", "--local-timezone=America/Los_Angeles"]

mcp_math:
_type: mcp_client
server:
transport: streamable-http
url: ${MATH_MCP_URL}
include:
- calculator__add
- calculator__subtract
- calculator__multiply
- calculator__divide

mcp_jira:
_type: mcp_client
server:
transport: streamable-http
url: ${CORPORATE_MCP_JIRA_URL}
auth_provider: mcp_oauth2_jira

authentication:
mcp_oauth2_jira:
_type: mcp_oauth2
server_url: ${CORPORATE_MCP_JIRA_URL}
redirect_uri: ${NAT_REDIRECT_URI:-http://localhost:8000/auth/redirect}

functions:
current_timezone:
_type: current_timezone

code_generation:
_type: code_generation
programming_language: Python
description: "Generate Python code for a task. Use this for any code-writing request."
llm_name: worker_llm

math_agent:
_type: tool_calling_agent
tool_names: [mcp_math]
llm_name: worker_llm
description: "Handles arithmetic and calculation requests."

time_agent:
_type: tool_calling_agent
tool_names: [mcp_time, current_timezone]
llm_name: worker_llm
description: "Answers date and time questions. Resolve the timezone before asking for the time."

jira_agent:
_type: tool_calling_agent
tool_names: [mcp_jira]
llm_name: worker_llm
description: "Looks up and summarizes Jira issues."

research_orchestrator:
_type: react_agent
tool_names: [math_agent, time_agent, jira_agent, code_generation]
llm_name: orchestrator_llm
verbose: true

workflow:
_type: reasoning_agent
llm_name: orchestrator_llm
augmented_fn: research_orchestrator
verbose: true
Loading
Loading