From 4dea194881fe212b293d10bd9b74c9cc20bbe21e Mon Sep 17 00:00:00 2001 From: sauravdev Date: Thu, 23 Apr 2026 14:36:05 +0530 Subject: [PATCH 1/2] Add local docs, skill, and sandbox policy on top of upstream - docs/inference: custom-llm-provider, switch-to-brev-nemotron-120b, switch-to-nemotron-super-120b - skill/nat: SKILL.md and reference docs - nemoclaw-sandbox-policy.yaml Rebased onto NVIDIA/NemoClaw main (at d9aced49). Prior edits to files that upstream refactored or deleted were dropped. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/inference/custom-llm-provider.md | 338 +++++++++++ .../inference/switch-to-brev-nemotron-120b.md | 220 +++++++ .../switch-to-nemotron-super-120b.md | 567 ++++++++++++++++++ nemoclaw-sandbox-policy.yaml | 102 ++++ skill/nat/SKILL.md | 162 +++++ skill/nat/references/a2a-server.md | 94 +++ skill/nat/references/custom-tools.md | 94 +++ skill/nat/references/examples.md | 72 +++ skill/nat/references/function-groups.md | 114 ++++ skill/nat/references/install-from-source.md | 20 + 10 files changed, 1783 insertions(+) create mode 100644 docs/inference/custom-llm-provider.md create mode 100644 docs/inference/switch-to-brev-nemotron-120b.md create mode 100644 docs/inference/switch-to-nemotron-super-120b.md create mode 100644 nemoclaw-sandbox-policy.yaml create mode 100644 skill/nat/SKILL.md create mode 100644 skill/nat/references/a2a-server.md create mode 100644 skill/nat/references/custom-tools.md create mode 100644 skill/nat/references/examples.md create mode 100644 skill/nat/references/function-groups.md create mode 100644 skill/nat/references/install-from-source.md diff --git a/docs/inference/custom-llm-provider.md b/docs/inference/custom-llm-provider.md new file mode 100644 index 00000000000..75fe4315792 --- /dev/null +++ b/docs/inference/custom-llm-provider.md @@ -0,0 +1,338 @@ +--- +title: + page: "Use a Custom LLM Provider with NemoClaw" + nav: "Custom LLM Provider" +description: "Configure NemoClaw to use any OpenAI-compatible LLM endpoint, including third-party providers like OpenAI, Azure, Anthropic, Perplexity, Google, and self-hosted models." +keywords: ["custom llm", "third-party model", "openai compatible", "api endpoint", "nemoclaw provider"] +topics: ["generative_ai", "ai_agents"] +tags: ["openclaw", "openshell", "inference_routing", "custom_provider"] +content: + type: how_to + difficulty: technical_beginner + audience: ["developer", "engineer"] +status: published +--- + + + +# Use a Custom LLM Provider with NemoClaw + +NemoClaw routes inference through any **OpenAI-compatible** `/v1/chat/completions` endpoint. +This guide shows how to point NemoClaw at a third-party LLM provider — no code changes required. + +## Overview + +NemoClaw uses three configuration values to connect to an LLM: + +| Setting | Description | Example | +|---|---|---| +| **Endpoint URL** | Base URL of the OpenAI-compatible API | `https://inference-api.nvidia.com/v1` | +| **API Key** | Authentication token for the endpoint | `sk-abc123...` | +| **Model ID** | Model identifier recognized by the endpoint | `azure/openai/gpt-5.4` | + +Any provider that exposes a standard `/v1/chat/completions` endpoint works out of the box. + +--- + +## Quick Start + +### Option A: Interactive Onboarding + +The simplest path — the wizard walks you through everything: + +```console +$ nemoclaw onboard +``` + +When prompted for the endpoint, select **"Custom"** and enter your provider's base URL. +Paste your API key when asked, then pick or type the model ID. + +### Option B: Non-Interactive (One Command) + +```console +$ export NEMOCLAW_API_KEY="your-api-key-here" +$ openclaw nemoclaw onboard \ + --endpoint custom \ + --endpoint-url "https://your-provider.com/v1" \ + --model "your/model-id" \ + --api-key "$NEMOCLAW_API_KEY" +``` + +### Option C: Manual Provider Setup + +If you already have a running sandbox and want to switch providers: + +```console +# 1. Set your API key +export NEMOCLAW_API_KEY="your-api-key-here" + +# 2. Create (or update) the provider +openshell provider create \ + --name my-provider \ + --type openai \ + --credential "NEMOCLAW_API_KEY=$NEMOCLAW_API_KEY" \ + --config "OPENAI_BASE_URL=https://your-provider.com/v1" + +# 3. Set the inference route +openshell inference set --provider my-provider --model "your/model-id" + +# 4. Verify +openshell inference get +``` + +--- + +## Provider Examples + +Below are ready-to-use configurations for popular LLM providers. + +### NVIDIA Inference API (Default) + +```bash +export NEMOCLAW_API_KEY="sk-your-nvidia-key" + +openshell provider create \ + --name nvidia-nim \ + --type openai \ + --credential "NEMOCLAW_API_KEY=$NEMOCLAW_API_KEY" \ + --config "OPENAI_BASE_URL=https://inference-api.nvidia.com/v1" + +openshell inference set --provider nvidia-nim --model azure/openai/gpt-5.4 +``` + +Available models include: +- `azure/openai/gpt-5.4` — OpenAI GPT-5.4 via Azure +- `nvidia/nemotron-3-super-120b-a12b` — Nemotron 3 Super 120B +- `nvidia/llama-3.1-nemotron-ultra-253b-v1` — Nemotron Ultra 253B +- `nvidia/llama-3.3-nemotron-super-49b-v1.5` — Nemotron Super 49B v1.5 + +### OpenAI Direct + +```bash +export NEMOCLAW_API_KEY="sk-your-openai-key" + +openshell provider create \ + --name openai-direct \ + --type openai \ + --credential "NEMOCLAW_API_KEY=$NEMOCLAW_API_KEY" \ + --config "OPENAI_BASE_URL=https://api.openai.com/v1" + +openshell inference set --provider openai-direct --model gpt-4o +``` + +### Azure OpenAI + +```bash +export NEMOCLAW_API_KEY="your-azure-api-key" + +openshell provider create \ + --name azure-openai \ + --type openai \ + --credential "NEMOCLAW_API_KEY=$NEMOCLAW_API_KEY" \ + --config "OPENAI_BASE_URL=https://your-resource.openai.azure.com/openai/deployments/your-deployment/v1" + +openshell inference set --provider azure-openai --model gpt-4o +``` + +### Anthropic (via OpenAI-Compatible Proxy) + +If you use a proxy that exposes Anthropic models through an OpenAI-compatible interface +(e.g., LiteLLM, OpenRouter): + +```bash +export NEMOCLAW_API_KEY="your-proxy-api-key" + +openshell provider create \ + --name anthropic-proxy \ + --type openai \ + --credential "NEMOCLAW_API_KEY=$NEMOCLAW_API_KEY" \ + --config "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + +openshell inference set --provider anthropic-proxy --model anthropic/claude-sonnet-4-6 +``` + +### Google Gemini (via OpenAI-Compatible Proxy) + +```bash +export NEMOCLAW_API_KEY="your-proxy-api-key" + +openshell provider create \ + --name gemini-proxy \ + --type openai \ + --credential "NEMOCLAW_API_KEY=$NEMOCLAW_API_KEY" \ + --config "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + +openshell inference set --provider gemini-proxy --model google/gemini-2.5-pro +``` + +### Local vLLM + +```bash +openshell provider create \ + --name vllm-local \ + --type openai \ + --credential "OPENAI_API_KEY=dummy" \ + --config "OPENAI_BASE_URL=http://host.openshell.internal:8000/v1" + +openshell inference set --provider vllm-local --model your-local-model +``` + +### Local Ollama + +```bash +openshell provider create \ + --name ollama-local \ + --type openai \ + --credential "OPENAI_API_KEY=ollama" \ + --config "OPENAI_BASE_URL=http://host.openshell.internal:11434/v1" + +openshell inference set --provider ollama-local --model llama3.3 +``` + +--- + +## Changing the Configuration After Setup + +### Update the API Key + +```bash +export NEMOCLAW_API_KEY="your-new-key" + +openshell provider update nvidia-nim \ + --credential "NEMOCLAW_API_KEY=$NEMOCLAW_API_KEY" +``` + +### Switch to a Different Endpoint + +```bash +openshell provider update nvidia-nim \ + --config "OPENAI_BASE_URL=https://new-endpoint.example.com/v1" +``` + +### Switch to a Different Model + +No rebuild required — just update the inference route: + +```bash +openshell inference set --provider nvidia-nim --model "new/model-id" +``` + +### Verify Current Configuration + +```bash +# Check inference route +openshell inference get + +# Check sandbox status +openclaw nemoclaw status +``` + +--- + +## Network Policy Configuration + +When using a custom endpoint, the sandbox network policy must allow outbound access +to the provider's hostname. The default policy (`nemoclaw-blueprint/policies/openclaw-sandbox.yaml`) +allows these hosts: + +- `integrate.api.nvidia.com:443` +- `inference-api.nvidia.com:443` + +To add a new endpoint, update the policy file under the `network_policies.nvidia.endpoints` section: + +```yaml +network_policies: + nvidia: + name: nvidia + endpoints: + # existing entries... + - host: your-provider.example.com + port: 443 + protocol: rest + enforcement: enforce + tls: terminate + rules: + - allow: { method: "*", path: "/**" } +``` + +Then apply the updated policy: + +```console +$ openshell policy set --file nemoclaw-blueprint/policies/openclaw-sandbox.yaml +``` + +For local providers (vLLM, Ollama), traffic goes through `host.openshell.internal` +which is already allowed by the gateway. + +--- + +## Configuration File Reference + +NemoClaw stores onboard configuration in `~/.nemoclaw/config.json`: + +```json +{ + "endpointType": "custom", + "endpointUrl": "https://your-provider.com/v1", + "ncpPartner": null, + "model": "your/model-id", + "profile": "ncp", + "credentialEnv": "NEMOCLAW_API_KEY", + "onboardedAt": "2026-03-25T00:00:00.000Z" +} +``` + +| Field | Description | +|---|---| +| `endpointType` | One of: `build`, `ncp`, `nim-local`, `vllm`, `ollama`, `custom` | +| `endpointUrl` | Full base URL of the inference API | +| `model` | Model ID to use for inference | +| `credentialEnv` | Environment variable name that holds the API key | +| `profile` | Blueprint profile name | + +--- + +## Supported Endpoint Types + +| Type | Description | Requires API Key | Default URL | +|---|---|---|---| +| `build` | NVIDIA Inference API | Yes | `https://inference-api.nvidia.com/v1` | +| `ncp` | NVIDIA Cloud Partner | Yes | User-provided | +| `custom` | Any OpenAI-compatible endpoint | Yes | User-provided | +| `nim-local` | Self-hosted NIM container | Yes | `http://nim-service.local:8000/v1` | +| `vllm` | Local vLLM server | No | `http://host.openshell.internal:8000/v1` | +| `ollama` | Local Ollama server | No | `http://host.openshell.internal:11434/v1` | + +--- + +## Troubleshooting + +### API key rejected + +- Confirm the key is valid for the endpoint: `curl -H "Authorization: Bearer $NEMOCLAW_API_KEY" https://your-endpoint/v1/models` +- NemoClaw accepts any key format — there is no prefix requirement. + +### Model not found + +- List available models: `curl -H "Authorization: Bearer $NEMOCLAW_API_KEY" https://your-endpoint/v1/models` +- Ensure the model ID matches exactly (case-sensitive). + +### Connection refused from sandbox + +- The sandbox network policy may be blocking the host. See [Network Policy Configuration](#network-policy-configuration) above. +- For local providers, use `http://host.openshell.internal:/v1` (not `localhost`). + +### Rate limit errors (429) + +- The provider is throttling requests. Wait and retry, or upgrade your plan. + +--- + +## Related Topics + +- [Switch Inference Models at Runtime](./switch-inference-providers.md) +- [Inference Profiles Reference](../reference/inference-profiles.md) +- [Network Policy — Approve Network Requests](../network-policy/approve-network-requests.md) diff --git a/docs/inference/switch-to-brev-nemotron-120b.md b/docs/inference/switch-to-brev-nemotron-120b.md new file mode 100644 index 00000000000..89dbbbffe19 --- /dev/null +++ b/docs/inference/switch-to-brev-nemotron-120b.md @@ -0,0 +1,220 @@ +--- +title: + page: "Fresh Setup: NemoClaw with Brev-Hosted Nemotron 3 Super 120B" + nav: "Brev Nemotron 3 Super 120B" +description: "End-to-end guide to switch NemoClaw from the default NVIDIA NIM cloud endpoint to a Brev-hosted Nemotron 3 Super 120B instance — no API key required." +keywords: ["nemotron", "nemotron-3-super-120b", "brev", "custom endpoint", "model switch"] +topics: ["generative_ai", "ai_agents"] +tags: ["openclaw", "openshell", "inference_routing", "nemotron", "brev"] +content: + type: how_to + difficulty: technical_beginner + audience: ["developer", "engineer"] +status: published +--- + + + +# Fresh Setup: NemoClaw with Brev-Hosted Nemotron 3 Super 120B + +Prerequisites: Docker running, openshell CLI installed, a Brev-hosted Nemotron 3 Super 120B +endpoint accessible. **No API key required** — the Brev endpoint has open access. + +--- + +## Step 1 — Verify your Brev endpoint + +Confirm the endpoint is healthy before making any changes: + +```bash +curl https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1/health/ready +# Expected: {"object":"health.response","message":"Service is ready","status":"ok"} +``` + +--- + +## Step 2 — Fix the Dockerfile model primary (line 70) + +The Dockerfile writes `openclaw.json` at build time. The model primary must use +the fully-qualified catalog key (`openai/nvidia/nemotron-3-super-120b-a12b`), not +the bare model ID. + +```python +# Before +'agents': {'defaults': {'model': {'primary': 'nvidia/nemotron-3-super-120b-a12b'}}}, + +# After +'agents': {'defaults': {'model': {'primary': 'openai/nvidia/nemotron-3-super-120b-a12b'}}}, +``` + +Everything else in the Dockerfile stays the same (provider key `openai`, baseUrl, apiKey, model ID in provider). + +--- + +## Step 3 — Fix `scripts/nemoclaw-start.sh` + +Three changes in this file: + +### 3a. Add model catalog registration to `fix_openclaw_config()` + +The `fix_openclaw_config()` function writes `openclaw.json` at runtime but was only +setting the model primary — it never registered the model in the catalog. Add the +provider/model block so OpenClaw knows what the model is: + +```python +cfg.setdefault('agents', {}).setdefault('defaults', {}).setdefault('model', {})['primary'] = 'openai/nvidia/nemotron-3-super-120b-a12b' + +# Register the model in the catalog so OpenClaw recognises it +models = cfg.setdefault('models', {}) +models['mode'] = 'merge' +openai_prov = models.setdefault('providers', {}).setdefault('openai', {}) +openai_prov['baseUrl'] = 'https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1' +openai_prov['apiKey'] = 'no-key-required' +openai_prov['api'] = 'openai-completions' +openai_prov['models'] = [ + { + 'id': 'nvidia/nemotron-3-super-120b-a12b', + 'name': 'Nemotron 3 Super 120B', + 'reasoning': False, + 'input': ['text'], + 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, + 'contextWindow': 131072, + 'maxTokens': 8192, + } +] +``` + +### 3b. Fix the `openclaw models set` argument (line 167) + +```bash +# Before +openclaw models set nvidia/nemotron-3-super-120b-a12b > /dev/null 2>&1 || true + +# After +openclaw models set openai/nvidia/nemotron-3-super-120b-a12b > /dev/null 2>&1 || true +``` + +### 3c. Reorder the startup sequence + +`fix_openclaw_config` must run **before** `openclaw models set`, otherwise the model +catalog is empty when OpenClaw tries to resolve the model. + +```bash +# Before (broken order) +echo 'Setting up NemoClaw...' +openclaw doctor --fix > /dev/null 2>&1 || true +openclaw models set openai/nvidia/nemotron-3-super-120b-a12b > /dev/null 2>&1 || true +write_auth_profile +export CHAT_UI_URL PUBLIC_PORT +fix_openclaw_config +openclaw plugins install /opt/nemoclaw > /dev/null 2>&1 || true + +# After (working order) +echo 'Setting up NemoClaw...' +export CHAT_UI_URL PUBLIC_PORT +fix_openclaw_config +write_auth_profile +openclaw doctor --fix > /dev/null 2>&1 || true +openclaw models set openai/nvidia/nemotron-3-super-120b-a12b > /dev/null 2>&1 || true +openclaw plugins install /opt/nemoclaw > /dev/null 2>&1 || true +``` + +--- + +## Step 4 — Build and verify + +```bash +docker build -t nemoclaw . +docker run --rm nemoclaw -c 'openclaw models list' +``` + +Expected output: + +``` +Model Input Ctx Local Auth Tags +openai/nvidia/nemotron-3-super-120b-a12b text 128k no yes default,configured +``` + +The model should show with full metadata (Input, Ctx, Auth populated) and no `missing` tag. + +--- + +## Step 5 — Run the onboard wizard + +```bash +nemoclaw onboard +``` + +--- + +## Step 6 — Verify end-to-end + +```bash +# Test via OpenClaw agent +ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \ + -o "ProxyCommand=$(which openshell) ssh-proxy --gateway-name nemoclaw --name my-assistant" \ + sandbox@openshell-my-assistant \ + 'openclaw agent --agent main --message "Say hello in one sentence"' +# Expected: a response from Nemotron 3 Super 120B +``` + +Dashboard available at: **http://127.0.0.1:18789/** + +--- + +## Why this fixes it + +OpenClaw resolves models as `{provider_key}/{model_id}`. The model is registered under +the `openai` provider with ID `nvidia/nemotron-3-super-120b-a12b`, so its catalog key is +`openai/nvidia/nemotron-3-super-120b-a12b`. + +The "Unknown model" error had two causes: + +1. **Missing provider prefix** — the model primary was `nvidia/nemotron-3-super-120b-a12b` + instead of `openai/nvidia/nemotron-3-super-120b-a12b`, so no catalog entry matched. + +2. **Missing catalog registration at runtime** — `fix_openclaw_config()` set the model + primary but never wrote the `models.providers.openai` block, so OpenClaw had no model + definition. The startup also ran `openclaw models set` before `fix_openclaw_config`, + meaning the catalog was empty when the command executed. + +The model ID inside the provider config stays as `nvidia/nemotron-3-super-120b-a12b` — +this is what gets sent to the Brev endpoint in API calls, which is what the endpoint expects. + +--- + +## Troubleshooting + +### `Unknown model: nvidia/nemotron-3-super-120b-a12b` + +The model primary is not using the fully-qualified catalog key. Ensure the primary is +`openai/nvidia/nemotron-3-super-120b-a12b` (with `openai/` prefix). + +Check with: +```bash +openclaw models list +``` + +### Model shows as `missing` in `openclaw models list` + +The model is referenced in the agent config but not found in any provider catalog. Verify +that `openclaw.json` has the provider config: + +```bash +cat ~/.openclaw/openclaw.json | python3 -m json.tool +``` + +Look for `models.providers.openai.models` containing an entry with +`"id": "nvidia/nemotron-3-super-120b-a12b"`. + +--- + +## Related Topics + +- [Use a Custom LLM Provider](./custom-llm-provider.md) +- [Switch Inference Models at Runtime](./switch-inference-providers.md) +- [Inference Profiles Reference](../reference/inference-profiles.md) +- [Network Policy — Approve Network Requests](../network-policy/approve-network-requests.md) diff --git a/docs/inference/switch-to-nemotron-super-120b.md b/docs/inference/switch-to-nemotron-super-120b.md new file mode 100644 index 00000000000..7d6fd98e452 --- /dev/null +++ b/docs/inference/switch-to-nemotron-super-120b.md @@ -0,0 +1,567 @@ +--- +title: + page: "Switch NemoClaw to Nemotron 3 Super 120B (Brev-Hosted)" + nav: "Nemotron 3 Super 120B" +description: "End-to-end guide to change the default NemoClaw model from OpenAI GPT-5.4 to NVIDIA Nemotron 3 Super 120B hosted on a Brev endpoint." +keywords: ["nemotron", "nemotron-3-super-120b", "brev", "custom endpoint", "model switch"] +topics: ["generative_ai", "ai_agents"] +tags: ["openclaw", "openshell", "inference_routing", "nemotron", "brev"] +content: + type: how_to + difficulty: technical_beginner + audience: ["developer", "engineer"] +status: published +--- + + + +# Switch NemoClaw to Nemotron 3 Super 120B (Brev-Hosted) + +This guide walks you through switching NemoClaw's default inference model from +OpenAI GPT-5.4 to **NVIDIA Nemotron 3 Super 120B** hosted on a Brev endpoint, +end-to-end. + +## Prerequisites + +- NemoClaw repository cloned locally +- Docker Desktop installed and running +- `openshell` CLI installed (`openshell --version`) +- `nemoclaw` CLI installed (`nemoclaw --help`) +- A running Nemotron 3 Super 120B endpoint (this guide uses Brev) + +### Verify Your Endpoint + +Before starting, confirm the endpoint is healthy: + +```bash +curl https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1/health/ready +``` + +Expected response: + +```json +{"object":"health.response","message":"Service is ready","status":"ok"} +``` + +Test a chat completion: + +```bash +curl https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "nvidia/nemotron-3-super-120b-a12b", + "messages": [{"role": "user", "content": "Say hello."}], + "max_tokens": 50, + "temperature": 0.2 + }' +``` + +--- + +## Overview of Changes + +Four files need to be modified: + +| # | File | What Changes | +|---|------|-------------| +| 1 | `Dockerfile` | Default model, endpoint URL, and model catalog patch | +| 2 | `scripts/nemoclaw-start.sh` | Startup default model and auth profile | +| 3 | `nemoclaw-blueprint/blueprint.yaml` | Default inference profile | +| 4 | `nemoclaw-blueprint/policies/openclaw-sandbox.yaml` | Network policy to allow Brev endpoint | + +--- + +## Step 1: Update the Dockerfile + +The Dockerfile builds the sandbox container image. It patches the OpenClaw model +catalog and writes the default `openclaw.json` configuration. + +Open `Dockerfile` and make three changes: + +### 1a. Change the Model Catalog Patch + +Find the section that patches the OpenClaw model catalog (around line 42): + +**Before:** + +```dockerfile +# Patch OpenClaw's model catalog: replace Meta Llama 3.3 70B with OpenAI GPT-5.4 +# (OpenClaw's embedded catalog doesn't include this model yet) +RUN find /usr/local/lib/node_modules/openclaw/dist -name "*.js" \ + -exec grep -l "meta/llama-3.3-70b-instruct" {} \; \ + | xargs -I{} sed -i \ + -e 's|meta/llama-3.3-70b-instruct|gpt-5.4|g' \ + -e 's|Meta Llama 3.3 70B Instruct|OpenAI GPT-5.4|g' \ + {} +# GPT-5.4 supports reasoning; flip the flag and bump maxTokens +RUN find /usr/local/lib/node_modules/openclaw/dist -name "*.js" \ + -exec grep -l "gpt-5.4" {} \; \ + | xargs -I{} python3 -c "import re,sys;\ +s=open('{}').read();\ +s=re.sub(r'(id:\\s*\"gpt-5.4.*?reasoning:\\s*)false',r'\\1true',s,flags=re.DOTALL);\ +s=re.sub(r'(id:\\s*\"gpt-5.4.*?maxTokens:\\s*)4096',r'\\g<1>8192',s,flags=re.DOTALL);\ +open('{}','w').write(s)" +``` + +**After:** + +```dockerfile +# Patch OpenClaw's model catalog: replace Meta Llama 3.3 70B with Nemotron 3 Super 120B +# (OpenClaw's embedded catalog doesn't include this model yet) +RUN find /usr/local/lib/node_modules/openclaw/dist -name "*.js" \ + -exec grep -l "meta/llama-3.3-70b-instruct" {} \; \ + | xargs -I{} sed -i \ + -e 's|meta/llama-3.3-70b-instruct|nvidia/nemotron-3-super-120b-a12b|g' \ + -e 's|Meta Llama 3.3 70B Instruct|Nemotron 3 Super 120B|g' \ + {} +# Bump maxTokens for Nemotron 3 Super 120B +RUN find /usr/local/lib/node_modules/openclaw/dist -name "*.js" \ + -exec grep -l "nvidia/nemotron-3-super-120b-a12b" {} \; \ + | xargs -I{} python3 -c "import re,sys;\ +s=open('{}').read();\ +s=re.sub(r'(id:\\s*\"nvidia/nemotron-3-super-120b-a12b.*?maxTokens:\\s*)4096',r'\\g<1>8192',s,flags=re.DOTALL);\ +open('{}','w').write(s)" +``` + +### 1b. Change the Default openclaw.json Configuration + +Find the `RUN python3 -c` block that writes `openclaw.json` (around line 66): + +**Before:** + +```dockerfile +# Write openclaw.json: set OpenAI as default provider, route through +# OpenAI API directly. openshell injects credentials via the provider configuration. +RUN python3 -c "\ +import json, os; \ +config = { \ + 'agents': {'defaults': {'model': {'primary': 'gpt-5.4'}}}, \ + 'models': {'mode': 'merge', 'providers': {'openai': { \ + 'baseUrl': 'https://api.openai.com/v1', \ + 'apiKey': 'openshell-managed', \ + 'api': 'openai-completions', \ + 'models': [{'id': 'gpt-5.4', 'name': 'OpenAI GPT-5.4', 'reasoning': True, 'input': ['text'], 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, 'contextWindow': 131072, 'maxTokens': 8192}] \ + }}} \ +}; \ +path = os.path.expanduser('~/.openclaw/openclaw.json'); \ +json.dump(config, open(path, 'w'), indent=2); \ +os.chmod(path, 0o600)" +``` + +**After:** + +```dockerfile +# Write openclaw.json: set Nemotron 3 Super 120B as default provider, route through +# Brev-hosted endpoint. No API key required for this endpoint. +RUN python3 -c "\ +import json, os; \ +config = { \ + 'agents': {'defaults': {'model': {'primary': 'nvidia/nemotron-3-super-120b-a12b'}}}, \ + 'models': {'mode': 'merge', 'providers': {'openai': { \ + 'baseUrl': 'https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1', \ + 'apiKey': 'no-key-required', \ + 'api': 'openai-completions', \ + 'models': [{'id': 'nvidia/nemotron-3-super-120b-a12b', 'name': 'Nemotron 3 Super 120B', 'reasoning': False, 'input': ['text'], 'cost': {'input': 0, 'output': 0, 'cacheRead': 0, 'cacheWrite': 0}, 'contextWindow': 131072, 'maxTokens': 8192}] \ + }}} \ +}; \ +path = os.path.expanduser('~/.openclaw/openclaw.json'); \ +json.dump(config, open(path, 'w'), indent=2); \ +os.chmod(path, 0o600)" +``` + +**Key changes:** +- Model ID: `gpt-5.4` → `nvidia/nemotron-3-super-120b-a12b` +- Model name: `OpenAI GPT-5.4` → `Nemotron 3 Super 120B` +- Base URL: `https://api.openai.com/v1` → `https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1` +- API key: `openshell-managed` → `no-key-required` (Brev endpoint has no auth) +- Reasoning: `True` → `False` + +--- + +## Step 2: Update the Startup Script + +Open `scripts/nemoclaw-start.sh` and make two changes: + +### 2a. Change the Default Model in fix_openclaw_config() + +Find the line inside the `fix_openclaw_config()` function (around line 33): + +**Before:** + +```python +cfg.setdefault('agents', {}).setdefault('defaults', {}).setdefault('model', {})['primary'] = 'gpt-5.4' +``` + +**After:** + +```python +cfg.setdefault('agents', {}).setdefault('defaults', {}).setdefault('model', {})['primary'] = 'nvidia/nemotron-3-super-120b-a12b' +``` + +### 2b. Change the Model Set Command + +Find the `openclaw models set` line (around line 171): + +**Before:** + +```bash +openclaw models set gpt-5.4 > /dev/null 2>&1 || true +``` + +**After:** + +```bash +openclaw models set nvidia/nemotron-3-super-120b-a12b > /dev/null 2>&1 || true +``` + +### 2c. Update the Auth Profile + +Since the Brev endpoint does not require an API key, update the `write_auth_profile()` function: + +**Before:** + +```bash +write_auth_profile() { + if [ -z "${OPENAI_API_KEY:-}" ]; then + return + fi + + python3 - <<'PYAUTH' +import json +import os +path = os.path.expanduser('~/.openclaw/agents/main/agent/auth-profiles.json') +os.makedirs(os.path.dirname(path), exist_ok=True) +json.dump({ + 'openai:manual': { + 'type': 'api_key', + 'provider': 'openai', + 'keyRef': {'source': 'env', 'id': 'OPENAI_API_KEY'}, + 'profileId': 'openai:manual', + } +}, open(path, 'w')) +os.chmod(path, 0o600) +PYAUTH +} +``` + +**After:** + +```bash +write_auth_profile() { + python3 - <<'PYAUTH' +import json +import os +path = os.path.expanduser('~/.openclaw/agents/main/agent/auth-profiles.json') +os.makedirs(os.path.dirname(path), exist_ok=True) +json.dump({ + 'brev-nemotron:manual': { + 'type': 'api_key', + 'provider': 'openai', + 'keyRef': {'source': 'literal', 'id': 'no-key-required'}, + 'profileId': 'brev-nemotron:manual', + } +}, open(path, 'w')) +os.chmod(path, 0o600) +PYAUTH +} +``` + +**Key changes:** +- Removed the `OPENAI_API_KEY` guard (no key needed) +- Changed provider name to `brev-nemotron` +- Changed key source from `env` to `literal` + +--- + +## Step 3: Update the Blueprint + +Open `nemoclaw-blueprint/blueprint.yaml` and update the default inference profile: + +**Before:** + +```yaml + default: + provider_type: "openai" + provider_name: "openai-direct" + endpoint: "https://api.openai.com/v1" + model: "gpt-5.4" + credential_env: "OPENAI_API_KEY" +``` + +**After:** + +```yaml + default: + provider_type: "openai" + provider_name: "brev-nemotron" + endpoint: "https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1" + model: "nvidia/nemotron-3-super-120b-a12b" + credential_env: "" +``` + +**Key changes:** +- Provider name: `openai-direct` → `brev-nemotron` +- Endpoint: OpenAI URL → Brev URL +- Model: `gpt-5.4` → `nvidia/nemotron-3-super-120b-a12b` +- Credential env: cleared (no API key required) + +--- + +## Step 4: Update the Network Policy + +The sandbox network policy must allow outbound traffic to the Brev endpoint. + +Open `nemoclaw-blueprint/policies/openclaw-sandbox.yaml` and update the `nvidia` network policy: + +**Before:** + +```yaml + nvidia: + name: nvidia + endpoints: + - host: api.openai.com + port: 443 + protocol: rest + enforcement: enforce + tls: terminate + rules: + - allow: { method: "*", path: "/**" } + binaries: + - { path: /usr/local/bin/claude } + - { path: /usr/local/bin/openclaw } + - { path: /usr/local/bin/node } + - { path: /usr/bin/curl } + - { path: "*" } +``` + +**After:** + +```yaml + nvidia: + name: nvidia + endpoints: + - host: nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com + port: 443 + protocol: rest + enforcement: enforce + tls: terminate + rules: + - allow: { method: "*", path: "/**" } + binaries: + - { path: /usr/local/bin/claude } + - { path: /usr/local/bin/openclaw } + - { path: /usr/local/bin/node } + - { path: /usr/bin/curl } + - { path: "*" } +``` + +**Key change:** +- Host: `api.openai.com` → `nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com` + +--- + +## Step 5: Deploy + +You have two options — full re-onboard or incremental update on a running sandbox. + +### Option A: Full Re-Onboard (Clean Setup) + +This rebuilds the container image and creates a fresh sandbox: + +```bash +# 1. Set a dummy API key (the onboard wizard still prompts for one) +export OPENAI_API_KEY="no-key-required" + +# 2. Clear the old sandbox registry +rm -f ~/.nemoclaw/sandboxes.json + +# 3. Run the onboard wizard +nemoclaw onboard +``` + +When prompted: +1. **Sandbox name** — press Enter for `my-assistant` (or type a name) +2. **Inference options** — choose `1` (OpenAI API — now routes to Brev) +3. **Policy presets** — press Enter to accept defaults + +### Option B: Incremental Update (Existing Sandbox) + +If you already have a running sandbox: + +```bash +# 1. Create the inference provider +openshell provider create \ + --name brev-nemotron \ + --type openai \ + --credential "OPENAI_API_KEY=no-key-required" \ + --config "OPENAI_BASE_URL=https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1" + +# 2. Set the inference route +openshell inference set \ + --provider brev-nemotron \ + --model "nvidia/nemotron-3-super-120b-a12b" + +# 3. Apply the updated network policy +openshell policy set \ + --policy nemoclaw-blueprint/policies/openclaw-sandbox.yaml \ + my-assistant + +# 4. Verify +openshell inference get +``` + +Expected output: + +``` +Provider: brev-nemotron +Model: nvidia/nemotron-3-super-120b-a12b +``` + +--- + +## Step 6: Verify + +### Check Sandbox Status + +```bash +openshell sandbox list +``` + +Expected: + +``` +NAME NAMESPACE CREATED PHASE +my-assistant openshell 2026-04-02 09:17:17 Ready +``` + +### Test Health from Inside the Sandbox + +Connect to the sandbox and test: + +```bash +nemoclaw my-assistant connect +``` + +Then inside the sandbox: + +```bash +curl -s https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1/health/ready +``` + +Expected: + +```json +{"object":"health.response","message":"Service is ready","status":"ok"} +``` + +### Test Chat Completions from Inside the Sandbox + +```bash +curl -s https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "model": "nvidia/nemotron-3-super-120b-a12b", + "messages": [{"role": "user", "content": "Say hello in one sentence."}], + "max_tokens": 50, + "temperature": 0.2 + }' +``` + +Expected: A valid JSON response with `"choices"` containing the model's reply. + +### Check Inference Configuration + +```bash +openshell inference get +``` + +Expected: + +``` +Provider: brev-nemotron +Model: nvidia/nemotron-3-super-120b-a12b +Version: 1 +``` + +--- + +## Summary of All Changes + +| File | Field | Before | After | +|------|-------|--------|-------| +| `Dockerfile` | Model catalog ID | `gpt-5.4` | `nvidia/nemotron-3-super-120b-a12b` | +| `Dockerfile` | Model catalog name | `OpenAI GPT-5.4` | `Nemotron 3 Super 120B` | +| `Dockerfile` | openclaw.json baseUrl | `https://api.openai.com/v1` | `https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1` | +| `Dockerfile` | openclaw.json apiKey | `openshell-managed` | `no-key-required` | +| `scripts/nemoclaw-start.sh` | Default model | `gpt-5.4` | `nvidia/nemotron-3-super-120b-a12b` | +| `scripts/nemoclaw-start.sh` | Auth profile | `openai:manual` (env-based) | `brev-nemotron:manual` (literal) | +| `nemoclaw-blueprint/blueprint.yaml` | provider_name | `openai-direct` | `brev-nemotron` | +| `nemoclaw-blueprint/blueprint.yaml` | endpoint | `https://api.openai.com/v1` | `https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1` | +| `nemoclaw-blueprint/blueprint.yaml` | model | `gpt-5.4` | `nvidia/nemotron-3-super-120b-a12b` | +| `nemoclaw-blueprint/blueprint.yaml` | credential_env | `OPENAI_API_KEY` | `""` (empty) | +| `policies/openclaw-sandbox.yaml` | nvidia endpoint host | `api.openai.com` | `nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com` | + +--- + +## Troubleshooting + +### Connection refused from sandbox + +The network policy may not be applied. Re-apply it: + +```bash +openshell policy set --policy nemoclaw-blueprint/policies/openclaw-sandbox.yaml my-assistant +``` + +### Model not found error + +Ensure the model ID is exactly `nvidia/nemotron-3-super-120b-a12b` (case-sensitive). +Verify available models on the endpoint: + +```bash +curl -s https://nemotron-3-super-120b-a12b-6ucal9h69.brevlab.com/v1/models +``` + +### Sandbox stuck in non-Ready state + +Check sandbox logs: + +```bash +nemoclaw my-assistant logs --follow +``` + +### Old model still being used + +The startup script may have cached the old config. Rebuild the sandbox: + +```bash +rm -f ~/.nemoclaw/sandboxes.json +openshell sandbox delete my-assistant +export OPENAI_API_KEY="no-key-required" +nemoclaw onboard +``` + +### Switching back to OpenAI GPT-5.4 + +To revert, either undo all file changes and re-onboard, or update the +inference route at runtime (no rebuild needed): + +```bash +openshell provider create --name openai-direct --type openai \ + --credential "OPENAI_API_KEY=$OPENAI_API_KEY" \ + --config "OPENAI_BASE_URL=https://api.openai.com/v1" + +openshell inference set --provider openai-direct --model gpt-5.4 +``` + +--- + +## Related Topics + +- [Use a Custom LLM Provider](./custom-llm-provider.md) +- [Switch Inference Models at Runtime](./switch-inference-providers.md) +- [Inference Profiles Reference](../reference/inference-profiles.md) +- [Network Policy — Approve Network Requests](../network-policy/approve-network-requests.md) diff --git a/nemoclaw-sandbox-policy.yaml b/nemoclaw-sandbox-policy.yaml new file mode 100644 index 00000000000..0d3ab1c6db6 --- /dev/null +++ b/nemoclaw-sandbox-policy.yaml @@ -0,0 +1,102 @@ +version: 1 + +filesystem_policy: + include_workdir: true + read_only: + - /usr + - /lib + - /proc + - /dev/urandom + - /app + - /etc + - /var/log + read_write: + - /sandbox + - /tmp + - /dev/null + +landlock: + compatibility: best_effort + +process: + run_as_user: sandbox + run_as_group: sandbox + +network_policies: + openai: + name: openai + endpoints: + - host: api.openai.com + port: 443 + access: full + - host: inference.local + port: 80 + access: full + + clawhub: + name: clawhub + endpoints: + - host: clawhub.com + port: 443 + protocol: rest + enforcement: enforce + tls: terminate + rules: + - allow: { method: GET, path: "/**" } + - allow: { method: POST, path: "/**" } + binaries: + - { path: /usr/local/bin/openclaw } + - { path: /usr/local/bin/node } + + openclaw_api: + name: openclaw_api + endpoints: + - host: openclaw.ai + port: 443 + protocol: rest + enforcement: enforce + tls: terminate + rules: + - allow: { method: GET, path: "/**" } + - allow: { method: POST, path: "/**" } + binaries: + - { path: /usr/local/bin/openclaw } + - { path: /usr/local/bin/node } + + openclaw_docs: + name: openclaw_docs + endpoints: + - host: docs.openclaw.ai + port: 443 + protocol: rest + enforcement: enforce + tls: terminate + rules: + - allow: { method: GET, path: "/**" } + binaries: + - { path: /usr/local/bin/openclaw } + - { path: /usr/local/bin/node } + + npm_registry: + name: npm_registry + endpoints: + - host: registry.npmjs.org + port: 443 + access: full + binaries: + - { path: /usr/local/bin/openclaw } + - { path: /usr/local/bin/npm } + - { path: /usr/local/bin/node } + + github: + name: github + endpoints: + - host: github.com + port: 443 + access: full + - host: api.github.com + port: 443 + access: full + binaries: + - { path: /usr/bin/gh } + - { path: /usr/bin/git } diff --git a/skill/nat/SKILL.md b/skill/nat/SKILL.md new file mode 100644 index 00000000000..c8b814bdd25 --- /dev/null +++ b/skill/nat/SKILL.md @@ -0,0 +1,162 @@ +--- +name: nat +description: "NVIDIA NeMo Agent Toolkit (NAT) — install, create workflows, add tools, run agents, evaluate performance, and publish as A2A/MCP servers. Use when: (1) installing or setting up NAT, (2) creating or editing workflow YAML configs, (3) adding built-in or custom tools/functions, (4) running agents with `nat run`, (5) evaluating or profiling workflows, (6) publishing workflows as A2A or MCP servers, (7) creating custom functions or function groups, (8) integrating with LangChain, LlamaIndex, CrewAI, or other frameworks. Trigger keywords: NAT, NeMo Agent Toolkit, nvidia-nat, nat run, nat workflow, nat eval, nat a2a, nat profiler, workflow.yml, react_agent, tool_calling_agent." +metadata: { "openclaw": { "requires": { "anyBins": ["nat", "pip", "uv"] }, "primaryEnv": "NVIDIA_API_KEY" } } +--- + +# NVIDIA NeMo Agent Toolkit (NAT) + +A flexible library for connecting enterprise agents to data sources and tools across any framework. + +- **Repo**: https://github.com/NVIDIA/NeMo-Agent-Toolkit +- **Docs**: https://docs.nvidia.com/nemo/agent-toolkit/latest/ + +## Installation + +```bash +# Core (pick one) +uv pip install nvidia-nat # recommended +pip install nvidia-nat + +# With framework extras +uv pip install "nvidia-nat[langchain]" # LangChain/LangGraph +uv pip install "nvidia-nat[llama-index]" # LlamaIndex +uv pip install "nvidia-nat[crewai]" # CrewAI +uv pip install "nvidia-nat[mcp]" # MCP +uv pip install "nvidia-nat[a2a]" # A2A +uv pip install "nvidia-nat[mem0ai]" # Mem0 memory +uv pip install "nvidia-nat[eval,profiling]" # Eval + profiling + +# Verify +nat --help && nat --version +``` + +For development install from source, see [references/install-from-source.md](references/install-from-source.md). + +## Quick Start + +```bash +export NVIDIA_API_KEY= +``` + +Create `workflow.yml`: + +```yaml +functions: + wikipedia_search: + _type: wiki_search + max_results: 2 + +llms: + nim_llm: + _type: nim + model_name: meta/llama-3.1-70b-instruct + temperature: 0.0 + +workflow: + _type: react_agent + tool_names: [wikipedia_search] + llm_name: nim_llm + verbose: true + parse_agent_response_max_retries: 3 +``` + +```bash +nat run --config_file workflow.yml --input "List five subspecies of Aardvarks" +``` + +## Workflow Configuration Structure + +Four main YAML sections: + +| Section | Purpose | +|---|---| +| `functions` | Tools (web search, calculators, custom) | +| `llms` | LLM provider configs (NIM, OpenAI, Azure, Bedrock) | +| `embedders` | Embedding models for vector storage | +| `workflow` | Agent type + wiring of tools and LLMs | + +## Agent Types (`_type` in `workflow`) + +- `react_agent` — Reasoning and acting +- `reasoning_agent` — Advanced reasoning +- `rewwo_agent` — Reasoning Without Observation +- `responses_api_agent` — OpenAI Responses API +- `tool_calling_agent` — Direct tool calling +- `automatic_memory_wrapper_agent` — Adds memory +- `router_agent` — Routes to different workflows +- `sequential_executor` — Sequential tool execution + +## Built-in Tools (`_type` in `functions`) + +`wiki_search`, `webpage_query`, `tavily_internet_search`, `arxiv_search`, `current_datetime`, `calculator`, `text_file_ingest`, and many more framework-specific tools. + +List all available components: + +```bash +nat info components -t function # Tools +nat info components -t llm_provider # LLMs +nat info components -t embedder # Embedders +``` + +## Common CLI Commands + +```bash +# Run workflow +nat run --config_file workflow.yml --input "question" + +# Override params without editing YAML +nat run --config_file workflow.yml --input "question" \ + --override llms.nim_llm.temperature 0.7 \ + --override llms.nim_llm.model_name meta/llama-3.3-70b-instruct + +# Create new workflow template +nat workflow create --workflow-dir examples my_workflow + +# Evaluate +nat eval --config_file eval_config.yml + +# Profile +nat profiler --config_file workflow.yml --input "test" + +# Red team +nat red-team --config_file workflow.yml + +# Workflow management +nat workflow reinstall my_workflow +nat workflow delete my_workflow +``` + +## Custom Tools and Function Groups + +For creating custom tools, function groups, and advanced patterns, see: + +- [references/custom-tools.md](references/custom-tools.md) — Writing custom functions, registration, and installation +- [references/function-groups.md](references/function-groups.md) — Shared config, namespacing, include/exclude, access levels + +## A2A Server + +Publish workflows as A2A agents for discovery and invocation by other A2A clients. + +```bash +# Start A2A server +nat a2a serve --config_file workflow.yml + +# Discover agent +nat a2a client discover --url http://localhost:10000 + +# Call agent +nat a2a client call --url http://localhost:10000 --message "What is 42 * 67?" +``` + +For full A2A configuration (auth, concurrency, Kubernetes), see [references/a2a-server.md](references/a2a-server.md). + +## Examples + +The repo includes examples organized by category: Getting Started, Agents, Advanced Agents, Control Flow, Frameworks, MCP/A2A, Evaluation, and more. See [references/examples.md](references/examples.md) for the full catalog and how to run them. + +```bash +# Run any example +uv pip install -e examples/ +nat run --config_file examples//configs/config.yml --input "test" +``` diff --git a/skill/nat/references/a2a-server.md b/skill/nat/references/a2a-server.md new file mode 100644 index 00000000000..f80d23061aa --- /dev/null +++ b/skill/nat/references/a2a-server.md @@ -0,0 +1,94 @@ +# A2A Server Configuration + +Publish NAT workflows as A2A agents for discovery and invocation. + +## Installation + +```bash +uv pip install "nvidia-nat[a2a]" +``` + +## Basic Usage + +```bash +nat a2a serve --config_file workflow.yml +# Server starts on http://localhost:10000 +# Agent Card at http://localhost:10000/.well-known/agent-card.json +``` + +## Server Options + +```bash +nat a2a serve --config_file workflow.yml \ + --host 0.0.0.0 \ + --port 11000 \ + --name "Calculator Agent" \ + --description "A calculator agent for mathematical operations" +``` + +## Configuration File Approach + +```yaml +general: + front_end: + _type: a2a + name: "Calculator Agent" + description: "A calculator agent for mathematical operations" + host: localhost + port: 10000 + public_base_url: "https://agents.example.com/calculator" + version: "1.0.0" + max_concurrency: 16 # default: 8 +``` + +## How Workflows Map to A2A + +- Workflow becomes an Agent +- Functions become Skills (auto-namespaced) +- Agent Card is auto-generated from workflow metadata + +## Client Commands + +```bash +# Discover +nat a2a client discover --url http://localhost:10000 +curl http://localhost:10000/.well-known/agent-card.json | jq + +# Call +nat a2a client call --url http://localhost:10000 --message "What is 42 * 67?" +``` + +## Get Full Schema + +```bash +nat info components -t front_end -q a2a +``` + +## Kubernetes / Ingress + +Set `public_base_url` so the Agent Card advertises the external URL: + +```yaml +general: + front_end: + _type: a2a + host: 0.0.0.0 + port: 10000 + public_base_url: ${NAT_PUBLIC_BASE_URL} +``` + +## Authentication + +A2A servers support OAuth2 with JWT token validation: +- Token signature via JWKS +- Issuer validation +- Expiration checks +- Scope and audience validation + +## Troubleshooting + +```bash +# Port in use +lsof -i :10000 +nat a2a serve --config_file config.yml --port 11000 +``` diff --git a/skill/nat/references/custom-tools.md b/skill/nat/references/custom-tools.md new file mode 100644 index 00000000000..8c7232df341 --- /dev/null +++ b/skill/nat/references/custom-tools.md @@ -0,0 +1,94 @@ +# Creating Custom Tools in NAT + +## Generate Template + +```bash +nat workflow create --workflow-dir examples my_custom_tool +``` + +This creates: + +``` +my_custom_tool/ +├── src/my_custom_tool/ +│ ├── __init__.py +│ ├── register.py # Component registration +│ └── my_custom_tool_function.py # Implementation +├── configs/config.yml # Workflow configuration +└── pyproject.toml # Dependencies +``` + +## Function Characteristics + +- **Type Safety**: Python type annotations for input/output validation +- **Dual Output**: `ainvoke()` for single output, `astream()` for streaming +- **Schemas**: Input/output schemas via Pydantic BaseModel +- **Asynchronous**: All operations are async +- **Composability**: Functions can be used as tools for other functions/agents + +## Basic Function Structure + +```python +from nemo_agent_toolkit.functions import FunctionBaseConfig +from nemo_agent_toolkit.components import EmbedderRef +from nemo_agent_toolkit.functions import register_function +from nemo_agent_toolkit.utils import LLMFrameworkEnum + +class MyCustomToolConfig(FunctionBaseConfig, name="my_custom_tool"): + param1: str + param2: int = 10 + embedder_name: EmbedderRef = "nvidia/nv-embedqa-e5-v5" + +@register_function(config_type=MyCustomToolConfig) +async def my_custom_tool_function(config: MyCustomToolConfig, builder: Builder): + # Access other components via builder: + # embeddings = await builder.get_embedder(config.embedder_name) + # llm = await builder.get_llm(config.llm_name) + + async def _inner(input_param: str) -> str: + return f"Processed: {input_param}" + + yield FunctionInfo.from_fn(_inner, description="Description of what your tool does") +``` + +## Install and Use + +```bash +uv pip install -e examples/my_custom_tool +nat run --config_file examples/my_custom_tool/configs/config.yml --input "test" +``` + +## Adding Tools to Existing Workflows + +### Quick override (no YAML edit) + +```bash +nat run --config_file workflow.yml --input "query" \ + --override functions.webpage_query.webpage_url https://example.com/docs +``` + +### Permanent change + +1. Add the tool in the `functions` section of workflow YAML +2. Add the tool name to `workflow.tool_names` +3. Run with updated config + +### Example: Adding webpage queries + +```yaml +functions: + docs_query: + _type: webpage_query + webpage_url: https://docs.example.com + description: "Search documentation" + embedder_name: nv-embedqa-e5-v5 + chunk_size: 512 + current_datetime: + _type: current_datetime + +workflow: + _type: react_agent + tool_names: [docs_query, current_datetime] + llm_name: nim_llm + verbose: true +``` diff --git a/skill/nat/references/examples.md b/skill/nat/references/examples.md new file mode 100644 index 00000000000..220a29fd3c9 --- /dev/null +++ b/skill/nat/references/examples.md @@ -0,0 +1,72 @@ +# NAT Examples Catalog + +All examples live in the `examples/` directory of the NeMo Agent Toolkit repo. + +## Running Any Example + +```bash +# Install from source first (see install-from-source.md) +uv pip install -e examples/ +nat run --config_file examples//configs/config.yml --input "test" +``` + +## Example Structure + +``` +example_name/ +├── src// +│ ├── __init__.py +│ ├── register.py # Component registration +│ └── .py # Implementation +├── configs/config.yml # Workflow configuration +├── data/ # Example data (if needed) +└── pyproject.toml +``` + +## Categories + +### Getting Started +- **simple_web_query** — Basic LangSmith documentation agent with internet search +- **simple_calculator** — Mathematical agent with arithmetic and time tools + +### Agents +- **react** — ReAct (Reasoning and Acting) agent +- **rewoo** — ReWOO (Reasoning WithOut Observation) pattern +- **tool_calling** — Direct function invocation agent +- **auto_memory_wrapper** — Automatic memory capture agent + +### Advanced Agents +- **alert_triage_agent** — Production-ready alert triage with LangGraph +- **mixture_of_agents** — Multi-agent with ReAct coordinating specialized tools + +### Control Flow +- **router_agent** — Routes requests to appropriate branches +- **sequential_executor** — Linear tool execution pipeline +- **parallel_executor** — Concurrent fan-out/fan-in stages + +### Frameworks +- **langchain_deep_research** — LangGraph agents with NAT +- **multi_frameworks** — Supervisor coordinating LangChain, LlamaIndex, Haystack +- **semantic_kernel_demo** — Travel planning with Microsoft Semantic Kernel + +### MCP / A2A +- **simple_calculator_mcp** — End-to-end MCP workflow (client + server) +- **simple_calculator_mcp_protected** — OAuth2-protected MCP workflow +- **currency_agent_a2a** — A2A client connecting to third-party services +- **math_assistant_a2a** — End-to-end A2A workflow + +### Evaluation / Profiling / Finetuning +- **email_phishing_analyzer** — Evaluation and profiling configs +- **dpo_tic_tac_toe** — DPO training using Test-Time Compute +- **rl_with_openpipe_art** — Reinforcement learning with OpenPipe ART + +### Other Categories +- Components, Custom Functions, Front Ends, Memory, Object Store, Human-in-the-Loop, UI + +## Using Examples as Starting Points + +1. Copy the example directory +2. Update `pyproject.toml` with your package name +3. Modify `configs/config.yml` for your tools, LLMs, parameters +4. Update source in `src/` as needed +5. Install with `uv pip install -e .` and run with `nat run` diff --git a/skill/nat/references/function-groups.md b/skill/nat/references/function-groups.md new file mode 100644 index 00000000000..b893a7e9ad0 --- /dev/null +++ b/skill/nat/references/function-groups.md @@ -0,0 +1,114 @@ +# Function Groups in NAT + +Function groups package multiple related functions together so they share configuration, context, and resources. + +## When to Use + +Use function groups when you have: +- Multiple functions needing the same DB connection, API client, or cache +- Related operations sharing config (credentials, endpoints, timeouts) +- A family of functions that benefit from namespacing (CRUD, math) +- Functions that need to share state or context + +Use individual functions when each is independent with no shared resources. + +## Writing a Function Group + +### 1. Define Config + +```python +from nemo_agent_toolkit.functions import FunctionGroupBaseConfig +from pydantic import Field + +class ObjectStoreConfig(FunctionGroupBaseConfig, name="object_store"): + endpoint: str = Field(description="S3 endpoint URL") + access_key: str = Field(description="S3 access key") + secret_key: str = Field(description="S3 secret key") + bucket: str = Field(description="S3 bucket name") +``` + +### 2. Implement Builder + +```python +from nemo_agent_toolkit.functions import register_function_group +from nemo_agent_toolkit.data_models.function import FunctionGroup + +@register_function_group(config_type=ObjectStoreConfig) +async def build_object_store(config: ObjectStoreConfig, builder: Builder): + s3_client = boto3.client('s3', + endpoint_url=config.endpoint, + aws_access_key_id=config.access_key, + aws_secret_access_key=config.secret_key + ) + + group = FunctionGroup(config=config, instance_name="storage") + + async def save_fn(filename: str, content: bytes) -> str: + s3_client.put_object(Bucket=config.bucket, Key=filename, Body=content) + return f"Saved {filename}" + + async def load_fn(filename: str) -> bytes: + response = s3_client.get_object(Bucket=config.bucket, Key=filename) + return response['Body'].read() + + group.add_function(name="save", fn=save_fn, description="Save file to storage") + group.add_function(name="load", fn=load_fn, description="Load file from storage") + + yield group +``` + +## YAML Configuration + +```yaml +function_groups: + storage: + _type: object_store + endpoint: "https://s3.amazonaws.com" + access_key: "${S3_ACCESS_KEY}" + secret_key: "${S3_SECRET_KEY}" + bucket: "my-bucket" + +workflow: + _type: react_agent + tool_names: [storage] # All functions in group + llm_name: my_llm +``` + +## Namespacing + +Functions are auto-namespaced: `instance_name__function_name` + +Example: group `storage` with functions `save`, `load` produces `storage__save`, `storage__load`. + +## include / exclude (Mutually Exclusive) + +```yaml +# Only expose specific functions +function_groups: + math: + _type: math_group + include: [add, multiply] + +# Hide specific functions from agents +function_groups: + math: + _type: math_group + exclude: [divide] +``` + +### Access Levels + +| Configuration | Programmatic | Global Registry | Agent Tools | +|---|---|---|---| +| No include/exclude | All | No | All | +| `include: [add]` | All | Only `add` | Only `add` | +| `exclude: [divide]` | All | No | All except `divide` | + +## Programmatic Access + +```python +storage_group = await builder.get_function_group("storage") +all_fns = await storage_group.get_all_functions() +save_fn = await storage_group.get_function("save") +result = await save_fn.ainvoke("test.txt", b"content") +``` diff --git a/skill/nat/references/install-from-source.md b/skill/nat/references/install-from-source.md new file mode 100644 index 00000000000..179d21325be --- /dev/null +++ b/skill/nat/references/install-from-source.md @@ -0,0 +1,20 @@ +# Install NAT from Source (Development) + +```bash +git clone -b main https://github.com/NVIDIA/NeMo-Agent-Toolkit.git nemo-agent-toolkit +cd nemo-agent-toolkit +git submodule update --init --recursive +git lfs install +git lfs fetch +git lfs pull +uv venv --python 3.13 --seed .venv +source .venv/bin/activate +uv sync --all-groups --extra most +``` + +Verify: + +```bash +nat --help +nat --version +``` From a6aa0e3509b12476eae3c4b03b87ac025f0485b2 Mon Sep 17 00:00:00 2001 From: sauravdev Date: Sat, 4 Jul 2026 09:41:50 +0530 Subject: [PATCH 2/2] feat(cli): add structured logging with --debug and NEMOCLAW_LOG_LEVEL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces src/lib/cli/logger.ts — a singleton Logger class with four levels (error < warn < info < debug) written exclusively to stderr so stdout remains clean for --json consumers. - `NEMOCLAW_LOG_LEVEL=debug` env var sets level at process start - `--debug` flag (inherited by all commands via NemoClawCommand.baseFlags) activates debug level and ISO-8601 timestamp prefixes - `-q / --quiet` flag suppresses info; shows only warn+error - `NEMOCLAW_DEBUG=1` and `DEBUG=*nemoclaw*` also activate debug level NemoClawCommand.init() reads both flags and configures the singleton before any command's run() method executes, so subcommands see the correct level without needing to parse flags themselves. Adds unit tests covering all level transitions and env-var pickup. Gradual migration of existing console.log/console.error call sites to log.info/log.error can be done incrementally on separate branches. Co-Authored-By: Claude Sonnet 4.6 --- src/lib/cli/logger.test.ts | 94 ++++++++++++++++++++ src/lib/cli/logger.ts | 118 ++++++++++++++++++++++++++ src/lib/cli/nemoclaw-oclif-command.ts | 19 +++++ 3 files changed, 231 insertions(+) create mode 100644 src/lib/cli/logger.test.ts create mode 100644 src/lib/cli/logger.ts diff --git a/src/lib/cli/logger.test.ts b/src/lib/cli/logger.test.ts new file mode 100644 index 00000000000..01d2ad5697f --- /dev/null +++ b/src/lib/cli/logger.test.ts @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { LogLevel } from "./logger"; + +// Re-import logger fresh for each test to reset singleton state +async function freshLogger() { + vi.resetModules(); + const mod = await import("./logger"); + return mod; +} + +describe("Logger", () => { + let stderrSpy: ReturnType; + + beforeEach(() => { + stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true); + stderrSpy.mockClear(); + vi.unstubAllEnvs(); + }); + + afterEach(() => { + stderrSpy.mockRestore(); + }); + + it("defaults to info level", async () => { + const { log } = await freshLogger(); + expect(log.level).toBe("info"); + }); + + it("reads NEMOCLAW_LOG_LEVEL from env", async () => { + vi.stubEnv("NEMOCLAW_LOG_LEVEL", "debug"); + const { log } = await freshLogger(); + expect(log.level).toBe("debug"); + }); + + it("suppresses debug messages at info level", async () => { + const { log } = await freshLogger(); + log.setLevel("info"); + log.debug("should not appear"); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it("shows debug messages after setDebug(true)", async () => { + const { log } = await freshLogger(); + log.setDebug(true); + log.debug("visible debug"); + expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining("visible debug")); + }); + + it("quiet mode suppresses info", async () => { + const { log } = await freshLogger(); + log.setQuiet(true); + log.info("suppressed info"); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it("quiet mode still shows warn", async () => { + const { log } = await freshLogger(); + log.setQuiet(true); + log.warn("visible warning"); + expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining("visible warning")); + }); + + it("error always shown", async () => { + const { log } = await freshLogger(); + log.setLevel("error" as LogLevel); + log.error("critical error"); + expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining("critical error")); + }); + + it("error suppressed below error level only for warn+info+debug", async () => { + const { log } = await freshLogger(); + log.setLevel("error" as LogLevel); + log.warn("should be suppressed"); + expect(stderrSpy).not.toHaveBeenCalled(); + }); + + it("debugObject emits JSON at debug level", async () => { + const { log } = await freshLogger(); + log.setLevel("debug"); + log.debugObject("context", { key: "val" }); + expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('"key"')); + }); + + it("debugObject suppressed at info level", async () => { + const { log } = await freshLogger(); + log.setLevel("info"); + log.debugObject("context", { key: "val" }); + expect(stderrSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/lib/cli/logger.ts b/src/lib/cli/logger.ts new file mode 100644 index 00000000000..4bcb1e2397b --- /dev/null +++ b/src/lib/cli/logger.ts @@ -0,0 +1,118 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Centralized logger for NemoClaw CLI. + * + * Levels (lowest → highest verbosity): + * error < warn < info < debug + * + * Default level: info (errors, warnings, and info messages shown). + * Quiet mode: warn (only warnings and errors shown). + * Debug mode: debug (all messages shown with timestamps). + * + * Configure via: + * NEMOCLAW_LOG_LEVEL=debug nemoclaw ... + * nemoclaw ... --debug (shorthand for debug level) + * nemoclaw ... -q / --quiet (suppresses info, shows warn+error) + */ + +export type LogLevel = "error" | "warn" | "info" | "debug"; + +const LEVEL_RANK: Record = { + error: 0, + warn: 1, + info: 2, + debug: 3, +}; + +function resolveLevel(): LogLevel { + const env = process.env.NEMOCLAW_LOG_LEVEL?.toLowerCase(); + if (env === "error" || env === "warn" || env === "info" || env === "debug") return env; + if (process.env.NEMOCLAW_DEBUG === "1" || process.env.DEBUG?.includes("nemoclaw")) return "debug"; + return "info"; +} + +class Logger { + private _level: LogLevel; + private _quiet: boolean; + private _timestamps: boolean; + + constructor() { + this._level = resolveLevel(); + this._quiet = false; + this._timestamps = this._level === "debug"; + } + + get level(): LogLevel { + return this._level; + } + + setLevel(level: LogLevel): void { + this._level = level; + this._timestamps = level === "debug"; + } + + setQuiet(quiet: boolean): void { + this._quiet = quiet; + if (quiet && LEVEL_RANK[this._level] > LEVEL_RANK["warn"]) { + this._level = "warn"; + } + } + + setDebug(debug: boolean): void { + if (debug) this.setLevel("debug"); + } + + isDebug(): boolean { + return this._level === "debug"; + } + + isQuiet(): boolean { + return this._quiet; + } + + private shouldLog(level: LogLevel): boolean { + return LEVEL_RANK[level] <= LEVEL_RANK[this._level]; + } + + private prefix(level: LogLevel): string { + if (!this._timestamps) return ""; + const ts = new Date().toISOString(); + return `[${ts}] [${level.toUpperCase()}] `; + } + + error(message: string, ...args: unknown[]): void { + if (!this.shouldLog("error")) return; + const parts = [this.prefix("error") + message, ...args.map(String)].join(" "); + process.stderr.write(parts + "\n"); + } + + warn(message: string, ...args: unknown[]): void { + if (!this.shouldLog("warn")) return; + const parts = [this.prefix("warn") + message, ...args.map(String)].join(" "); + process.stderr.write(parts + "\n"); + } + + info(message: string, ...args: unknown[]): void { + if (!this.shouldLog("info")) return; + const parts = [this.prefix("info") + message, ...args.map(String)].join(" "); + process.stderr.write(parts + "\n"); + } + + debug(message: string, ...args: unknown[]): void { + if (!this.shouldLog("debug")) return; + const parts = [this.prefix("debug") + message, ...args.map(String)].join(" "); + process.stderr.write(parts + "\n"); + } + + /** Log a structured object at debug level. Redacts nothing — call only with safe data. */ + debugObject(label: string, obj: unknown): void { + if (!this.shouldLog("debug")) return; + const ts = this._timestamps ? `[${new Date().toISOString()}] [DEBUG] ` : ""; + process.stderr.write(`${ts}${label}: ${JSON.stringify(obj, null, 2)}\n`); + } +} + +/** Singleton logger shared across all NemoClaw modules. */ +export const log = new Logger(); diff --git a/src/lib/cli/nemoclaw-oclif-command.ts b/src/lib/cli/nemoclaw-oclif-command.ts index 031a979e77b..c3d02d19ca5 100644 --- a/src/lib/cli/nemoclaw-oclif-command.ts +++ b/src/lib/cli/nemoclaw-oclif-command.ts @@ -3,6 +3,7 @@ import { Command, Flags } from "@oclif/core"; +import { log } from "./logger"; import { redactForLog } from "../security/redact"; export type CommandExitResult = { @@ -20,8 +21,26 @@ export type CommandExitResult = { export abstract class NemoClawCommand extends Command { static baseFlags = { help: Flags.help({ char: "h" }), + debug: Flags.boolean({ + description: "Enable debug output (equivalent to NEMOCLAW_LOG_LEVEL=debug)", + env: "NEMOCLAW_DEBUG", + default: false, + hidden: false, + }), + quiet: Flags.boolean({ + char: "q", + description: "Suppress informational output; show only warnings and errors", + default: false, + }), }; + async init(): Promise { + await super.init(); + const { flags } = await this.parse(this.constructor as typeof NemoClawCommand); + if ((flags as { debug?: boolean }).debug) log.setDebug(true); + if ((flags as { quiet?: boolean }).quiet) log.setQuiet(true); + } + protected logJson(json: unknown): void { console.log(JSON.stringify(redactForLog(json), null, 2)); }