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 +``` 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)); }