From 3ac928a3a09047c872e05bae3f1c4fb5b222fa37 Mon Sep 17 00:00:00 2001 From: Yuchen Zhang <134643420+yczhang-nv@users.noreply.github.com> Date: Wed, 30 Jul 2025 12:05:13 -0700 Subject: [PATCH 1/9] Fix outdated path in `pyproject.toml` in `text_file_ingest` (#524) Closes nvbugs-5425990 ## By Submitting this PR I confirm: - I am familiar with the [Contributing Guidelines](https://github.com/NVIDIA/NeMo-Agent-Toolkit/blob/develop/docs/source/resources/contributing.md). - We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license. - Any contribution which contains commits that are not Signed-Off will not be accepted. - When the PR is ready for review, new or existing tests cover these changes. - When the PR is ready for review, the documentation is up to date with these changes. Authors: - Yuchen Zhang (https://github.com/yczhang-nv) Approvers: - Will Killian (https://github.com/willkill07) URL: https://github.com/NVIDIA/NeMo-Agent-Toolkit/pull/524 From 1b467ef330c97543b1dc9b9bb9370e6366d890dd Mon Sep 17 00:00:00 2001 From: David Gardner Date: Thu, 14 Aug 2025 13:38:18 -0700 Subject: [PATCH 2/9] tagging develop as v1.3a From 671166925dc923054f205fbd31c6f812b9b5a78d Mon Sep 17 00:00:00 2001 From: David Gardner Date: Wed, 20 Aug 2025 13:25:10 -0700 Subject: [PATCH 3/9] Creating RC1 tag for 1.2.1 Signed-off-by: David Gardner From 43c05a6a2f66c51ba528ebe73ebf304ada4f0644 Mon Sep 17 00:00:00 2001 From: Yasha Pushak Date: Wed, 17 Dec 2025 13:09:31 -0800 Subject: [PATCH 4/9] Initial implementation plan Signed-off-by: Yasha Pushak --- ...entspec-milestone-1-implementation-plan.md | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 docs/source/proposals/agentspec-milestone-1-implementation-plan.md diff --git a/docs/source/proposals/agentspec-milestone-1-implementation-plan.md b/docs/source/proposals/agentspec-milestone-1-implementation-plan.md new file mode 100644 index 0000000000..0c75621771 --- /dev/null +++ b/docs/source/proposals/agentspec-milestone-1-implementation-plan.md @@ -0,0 +1,165 @@ +# Milestone 1: Agent Spec Import Support in NeMo Agent Toolkit + +Author: Oracle (Yasha Pushak) • Reviewer: NVIDIA (NeMo team) + +Status: Draft (Implementation Plan) + +## Scope + +Add a new workflow/function type to NeMo Agent Toolkit that executes Agent Spec configurations using the Agent Spec → LangGraph adapter. This enables representing richer agentic patterns (flows, branches/loops, multi‑agent) inside NeMo, while reusing NeMo’s tooling (evaluation, profiling, observability). + +Non-goals for this milestone: +- Multi-runtime selection (CrewAI, WayFlow, …) — planned for Milestone 2. +- Sub-component tuning via Agent Spec graph introspection — planned for Milestone 3. +- New UI or API surface beyond what is required to run an Agent Spec workflow. + +## High-level Design + +- Introduce a new function config type: `AgentSpecWorkflowConfig` with `_type: agent_spec`. +- At build time, use the Agent Spec LangGraph adapter to convert an Agent Spec document into a `langgraph` CompiledStateGraph and run it as the workflow implementation. +- Reuse NeMo’s Builder to construct a tool registry compatible with LangGraph/LangChain tools from NAT-configured tools. +- Keep inputs/outputs aligned with NeMo’s `ChatRequest`/`ChatResponse` for consistent UX. +- Hook into NeMo profiling/observability using existing `LLMFrameworkEnum.LANGCHAIN` wrapper integration. + +## Key Integration Points + +- Registration: `@register_function(config_type=AgentSpecWorkflowConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN])`. +- Config model: subclass `AgentBaseConfig` (for parity with `react_agent`), add fields to accept the Agent Spec payload and minimal runtime options. +- Adapter usage: `langgraph_agentspec_adapter.AgentSpecLoader` to `load_yaml`/`load_json` or from a structured Pydantic field converted to YAML/JSON. +- Tools: leverage `builder.get_tools(..., wrapper_type=LLMFrameworkEnum.LANGCHAIN)` to obtain LangChain-compatible tools, then pass them into the adapter `tool_registry` by name. +- Execution: compile LangGraph component from Agent Spec and `ainvoke` based on `ChatRequest` messages; return `ChatResponse` with basic usage stats, consistent with existing agents. + +## Configuration Schema (initial) + +Example NAT YAML: + +```yaml +workflow: + _type: agent_spec + description: Agent Spec workflow + # Exactly one of the following should be provided + agentspec_yaml: | + component_type: Agent + id: bdd2369b-82e6-488f-be2c-44f05b244cab + name: writing agent + description: Agent to help write blog articles + system_prompt: "You're a helpful writing assistant..." + inputs: + - title: user_name + type: string + llm_config: + component_type: VllmConfig + model_id: gpt-oss-120b + url: http://url.to.my.vllm.server/ + tools: + - component_type: ServerTool + name: pretty_formatting + description: Format paragraph spacing and indentation + inputs: + - title: paragraph + type: string + outputs: + - title: formatted_paragraph + type: string + # Optional alternatives to inline YAML + # agentspec_json: "{...}" + # agentspec_path: path/to/agent_spec.yaml + tool_names: [pretty_formatting] # map to NeMo/LC tools where applicable + verbose: true + max_history: 15 +``` + +Notes: +- We support exactly one of: `agentspec_yaml`, `agentspec_json`, or `agentspec_path`. +- `tool_names` are optional. If provided, we populate the adapter `tool_registry` with LC-compatible wrappers for those names. If the Agent Spec includes ServerTools with embedded `func`, both can coexist; the registry complements the spec. +- We reuse standard agent flags (`verbose`, `max_history`, `log_response_max_chars`, etc.) when easy; reserved flags beyond minimal needs can be deferred. + +## Detailed Tasks + +1) Data model and registration +- Add `AgentSpecWorkflowConfig` in `nat/agent/agentspec/register.py`: + - Base: `AgentBaseConfig`. + - Name: `agent_spec`. + - Fields: + - `description: str = "Agent Spec Workflow"`. + - `agentspec_yaml: str | None`. + - `agentspec_json: str | None`. + - `agentspec_path: str | None`. + - `tool_names: list[FunctionRef | FunctionGroupRef] = []` (optional). + - `max_history: int = 15`, `verbose: bool = False`, `log_response_max_chars: int | None = None`. + - Validation: exactly one of `agentspec_yaml/json/path` must be set. +- Register build function with `@register_function(..., framework_wrappers=[LLMFrameworkEnum.LANGCHAIN])`. + +2) Builder integration (execution function) +- Resolve the Agent Spec payload (from YAML/JSON/path) into a string. +- Build LC-compatible tools via `builder.get_tools(tool_names=..., wrapper_type=LLMFrameworkEnum.LANGCHAIN)` and create a dict `{name: tool}` to pass as `tool_registry` to `AgentSpecLoader`. +- Instantiate adapter: `AgentSpecLoader(tool_registry=..., checkpointer=None, config=None)`. +- Load to LangGraph component via `load_yaml`/`load_json`. +- Implement NAT function body accepting `ChatRequestOrMessage`: + - Convert to `ChatRequest`. + - Trim/limit history based on `max_history` (reuse logic from `react_agent`). + - Prepare input state/messages as required by the compiled graph: + - If the compiled component expects `{"messages": [...]}`, pass aligned payload. + - Otherwise, pass the minimal input the adapter expects; start with messages-only path as per adapter examples. + - `await graph.ainvoke(state, config={"recursion_limit": safe_default})`. + - Extract final message content and build `ChatResponse` with basic token usage. + +3) Dependency management +- Runtime dependency on `langgraph-agentspec-adapter` and `pyagentspec`. +- Add an extra in NeMo toolkit packaging (e.g., `agentspec`) and document installation: `pip install nat[agentspec]`. +- For source builds in this mono-repo, ensure import path resolution works during tests (use dev requirements or local path instruction in docs). + +4) Error handling and validation +- Clear errors for: + - Missing/invalid Agent Spec payload. + - Both/none of YAML/JSON/path provided. + - Tool name not found in NAT registry when specified. + - Adapter conversion/type errors. +- Log with existing agent log prefix; respect `verbose` and `log_response_max_chars` where applicable. + +5) Observability/profiling +- Mark function with `framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]` to enable existing profiler hooks. +- Ensure intermediate steps respect NAT callback/logging conventions where feasible in this milestone (best effort; deep step-by-step surfacing can be deferred). + +6) Documentation +- Add a new page under `docs/source/components/agents/agent-spec.md`: + - Purpose and capabilities. + - Installation instructions (extra deps). + - Minimal YAML example and tool mapping guidance. + - Notes/limitations in Milestone 1. +- Update tutorials index and examples list. + +7) Testing +- Unit tests: + - Config validation (exactly-one-of source fields). + - Tool registry mapping (with and without `tool_names`). +- Adapter smoke test: + - Use a tiny Agent Spec with a single tool or echo flow; verify invocation returns a `ChatResponse`. + - Mark network/LLM usage as skipped unless a local model is configured; prefer a spec that does not require external LLM for the smoke test (e.g., a simple tool graph), or mock the adapter/LLM call in tests. + +## Open Questions / Assumptions + +- Tool mapping precedence: if an Agent Spec defines ServerTools and NAT also provides `tool_names`, we will merge, with NAT tools overwriting duplicate names in the adapter registry (document this behavior). +- Input schema: In Milestone 1 we standardize on `ChatRequest` messages as input; richer parameter passing (matching Agent Spec inputs) can be extended later. +- Checkpointing: adapter supports a `Checkpointer`; we defer integration to a later milestone unless straightforward to pass through. +- Streaming: we target non-streaming response first; streaming support can be added post-M1 if needed. + +## Risks and Mitigations + +- Dependency drift between LangGraph/LangChain versions: pin compatible versions in the `agentspec` extra and CI. +- Mismatch between adapter’s expected state shape and our message wrapper: start with the adapter’s documented pattern using `{"messages": [...]}` and add a thin translator if necessary. +- Tool contract mismatch: validate tool signatures with simple probes and surface clear errors. + +## Delivery Checklist + +- [ ] `AgentSpecWorkflowConfig` added and registered. +- [ ] Build function executes adapter graph and returns `ChatResponse`. +- [ ] Packaging: `agentspec` extra with pinned dependencies and docs. +- [ ] Docs page with examples and limitations. +- [ ] Unit tests + smoke test. + +## Appendix: References + +- NAT registration pattern: `nat/agent/react_agent/register.py` (`ReActAgentWorkflowConfig`, `react_agent_workflow`). +- Type registry and wrappers: `nat/cli/register_workflow.py`, `LLMFrameworkEnum.LANGCHAIN`. +- Adapter entrypoints: `langgraph_agentspec_adapter.AgentSpecLoader` (load_yaml/json/component). From cb064f06d11123192d46c894f5b08b70a22d47df Mon Sep 17 00:00:00 2001 From: Yasha Pushak Date: Fri, 19 Dec 2025 09:38:46 -0800 Subject: [PATCH 5/9] First working draft Signed-off-by: Yasha Pushak --- docs/source/components/agents/agent-spec.md | 34 +++++ ...entspec-milestone-1-implementation-plan.md | 15 +- pyproject.toml | 1 + scripts/agentspec_smoke.py | 58 ++++++++ src/nat/agent/agentspec/config.py | 52 +++++++ src/nat/agent/agentspec/register.py | 137 ++++++++++++++++++ src/nat/agent/register.py | 1 + tests/conftest.py | 4 +- tests/test_agentspec_config.py | 38 +++++ tests/test_agentspec_smoke.py | 53 +++++++ 10 files changed, 388 insertions(+), 5 deletions(-) create mode 100644 docs/source/components/agents/agent-spec.md create mode 100644 scripts/agentspec_smoke.py create mode 100644 src/nat/agent/agentspec/config.py create mode 100644 src/nat/agent/agentspec/register.py create mode 100644 tests/test_agentspec_config.py create mode 100644 tests/test_agentspec_smoke.py diff --git a/docs/source/components/agents/agent-spec.md b/docs/source/components/agents/agent-spec.md new file mode 100644 index 0000000000..717af56752 --- /dev/null +++ b/docs/source/components/agents/agent-spec.md @@ -0,0 +1,34 @@ +# Agent Spec Workflow (Milestone 1) + +This workflow allows running an [Agent Spec] configuration inside NeMo Agent Toolkit by converting it to a LangGraph component via the Agent Spec → LangGraph adapter. + +## Install + +- Install optional extra: + +```bash +pip install 'nvidia-nat[agentspec]' +``` + +## Example configuration + +```yaml +workflow: + _type: agent_spec + description: Agent Spec workflow + agentspec_path: path/to/agent_spec.yaml # or agentspec_yaml / agentspec_json + tool_names: [pretty_formatting] + max_history: 15 + verbose: true +``` + +Exactly one of `agentspec_yaml`, `agentspec_json`, or `agentspec_path` must be provided. + +## Notes and limitations + +- Tools: NAT tools provided in `tool_names` are exposed to the adapter `tool_registry` by name. If the Agent Spec also defines tools, the registries are merged; duplicate names are overwritten by NAT tools. +- I/O: Inputs are standard `ChatRequest` messages; the workflow returns a `ChatResponse`. +- Streaming: Non‑streaming response in M1. +- Checkpointing: Not wired in M1. + +[Agent Spec]: https://github.com/oracle/agent-spec diff --git a/docs/source/proposals/agentspec-milestone-1-implementation-plan.md b/docs/source/proposals/agentspec-milestone-1-implementation-plan.md index 0c75621771..35610164db 100644 --- a/docs/source/proposals/agentspec-milestone-1-implementation-plan.md +++ b/docs/source/proposals/agentspec-milestone-1-implementation-plan.md @@ -152,11 +152,18 @@ Notes: ## Delivery Checklist -- [ ] `AgentSpecWorkflowConfig` added and registered. -- [ ] Build function executes adapter graph and returns `ChatResponse`. -- [ ] Packaging: `agentspec` extra with pinned dependencies and docs. +- [x] `AgentSpecWorkflowConfig` added and registered. +- [x] Build function executes adapter graph and returns `ChatResponse`. +- [x] Packaging: `agentspec` extra with pinned dependencies and docs stub. - [ ] Docs page with examples and limitations. -- [ ] Unit tests + smoke test. +- [x] Unit tests: config validation. +- [ ] Smoke test with minimal Agent Spec graph. + +## Plan Updates + +- Added optional dependency extra `agentspec` in `pyproject.toml` with `pyagentspec` and `langgraph-agentspec-adapter`. +- Implemented `AgentSpecWorkflowConfig` at `src/nat/agent/agentspec/register.py:1` and wired registration in `src/nat/agent/register.py:1`. +- Added basic config validation tests in `tests/test_agentspec_config.py:1`. ## Appendix: References diff --git a/pyproject.toml b/pyproject.toml index 99e2217157..a5ee27187c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ crewai = ["nvidia-nat-crewai"] data-flywheel = ["nvidia-nat-data-flywheel"] ingestion = ["nvidia-nat-ingestion"] # meta-package langchain = ["nvidia-nat-langchain"] +agentspec = ["nvidia-nat-langchain", "pyagentspec>=0.1", "langgraph-agentspec-adapter>=0.1"] # TODO: How do we actually reference the langgraph adapter as a dependency? llama-index = ["nvidia-nat-llama-index"] mcp = ["nvidia-nat-mcp"] mem0ai = ["nvidia-nat-mem0ai"] diff --git a/scripts/agentspec_smoke.py b/scripts/agentspec_smoke.py new file mode 100644 index 0000000000..4c021b15b1 --- /dev/null +++ b/scripts/agentspec_smoke.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import asyncio +import sys +import types + + +async def main(): + # Ensure NAT src is importable if running from repo root + import os + repo_root = os.path.dirname(os.path.abspath(__file__)) + src_dir = os.path.join(os.path.dirname(repo_root), "src") + if src_dir not in sys.path: + sys.path.insert(0, src_dir) + + # Force registration imports + import nat.agent.register # noqa: F401 + + # Create a fake adapter module that returns a stub component + class StubComponent: + async def ainvoke(self, value): + if isinstance(value, dict) and "messages" in value: + msgs = value["messages"] + last_user = next((m.get("content") for m in reversed(msgs) if m.get("role") == "user"), "") + return {"output": last_user} + return {"output": str(value)} + + class StubLoader: + def __init__(self, *args, **kwargs): + pass + + def load_yaml(self, _): + return StubComponent() + + fake_mod = types.ModuleType("langgraph_agentspec_adapter.agentspecloader") + fake_mod.AgentSpecLoader = StubLoader + sys.modules["langgraph_agentspec_adapter"] = types.ModuleType("langgraph_agentspec_adapter") + sys.modules["langgraph_agentspec_adapter.agentspecloader"] = fake_mod + + # Import registers agent workflows (including Agent Spec) + import nat.agent.agentspec.register # noqa: F401 + from nat.agent.agentspec.config import AgentSpecWorkflowConfig + from nat.builder.workflow_builder import WorkflowBuilder + + spec_yaml = """ +component_type: Agent +name: echo-agent +description: echo +""" + + cfg = AgentSpecWorkflowConfig(llm_name="dummy", agentspec_yaml=spec_yaml, tool_names=[]) + async with WorkflowBuilder() as builder: + fn = await builder.set_workflow(cfg) + out = await fn.acall_invoke(input_message="hello agentspec") + print("OK:", out) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/nat/agent/agentspec/config.py b/src/nat/agent/agentspec/config.py new file mode 100644 index 0000000000..9a0448b728 --- /dev/null +++ b/src/nat/agent/agentspec/config.py @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +from pathlib import Path +from pydantic import Field, model_validator + +from nat.data_models.agent import AgentBaseConfig +from nat.data_models.component_ref import FunctionGroupRef, FunctionRef + + +class AgentSpecWorkflowConfig(AgentBaseConfig, name="agent_spec"): + """ + NAT function that executes an Agent Spec configuration via the LangGraph adapter. + + Provide exactly one of agentspec_yaml, agentspec_json, or agentspec_path. + Optionally supply tool_names to make NAT/LC tools available to the Agent Spec runtime. + """ + + description: str = Field(default="Agent Spec Workflow", description="Description of this workflow.") + + agentspec_yaml: str | None = Field(default=None, description="Inline Agent Spec YAML content") + agentspec_json: str | None = Field(default=None, description="Inline Agent Spec JSON content") + agentspec_path: str | None = Field(default=None, description="Path to an Agent Spec YAML/JSON file") + + tool_names: list[FunctionRef | FunctionGroupRef] = Field( + default_factory=list, description="Optional list of tool names/groups to expose to the Agent Spec runtime." + ) + + max_history: int = Field(default=15, description="Maximum number of messages to keep in conversation history.") + + @model_validator(mode="after") + def _validate_sources(self): + provided = [self.agentspec_yaml, self.agentspec_json, self.agentspec_path] + cnt = sum(1 for v in provided if v) + if cnt != 1: + raise ValueError("Exactly one of agentspec_yaml, agentspec_json, or agentspec_path must be provided") + return self + + +def read_agentspec_payload(config: AgentSpecWorkflowConfig) -> tuple[str, str]: + """Return (format, payload_str) where format is 'yaml' or 'json'.""" + if config.agentspec_yaml: + return ("yaml", config.agentspec_yaml) + if config.agentspec_json: + return ("json", config.agentspec_json) + assert config.agentspec_path + path = Path(config.agentspec_path) + text = path.read_text(encoding="utf-8") + ext = path.suffix.lower() + fmt = "json" if ext == ".json" else "yaml" + return (fmt, text) + diff --git a/src/nat/agent/agentspec/register.py b/src/nat/agent/agentspec/register.py new file mode 100644 index 0000000000..e8c20311f0 --- /dev/null +++ b/src/nat/agent/agentspec/register.py @@ -0,0 +1,137 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import io +import logging +from typing import Any + +from nat.builder.framework_enum import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.api_server import ChatRequest, ChatRequestOrMessage, ChatResponse, Message, Usage +from nat.utils.type_converter import GlobalTypeConverter +from .config import AgentSpecWorkflowConfig, read_agentspec_payload + +logger = logging.getLogger(__name__) + + +def _to_plain_messages(messages: list[Any]) -> list[dict[str, Any]]: + plain: list[dict[str, Any]] = [] + for m in messages: + # Accept either NAT Message models or LangChain BaseMessage dicts + role = None + content = None + if isinstance(m, dict): + role = m.get("role") + content = m.get("content") + else: + # Try NAT Message model + if hasattr(m, "role"): + role = getattr(m.role, "value", None) or str(getattr(m, "role")) + # Various content shapes + if hasattr(m, "content"): + c = getattr(m, "content") + if isinstance(c, str): + content = c + else: + try: + buf = io.StringIO() + for part in c: + if hasattr(part, "text"): + buf.write(str(getattr(part, "text"))) + else: + buf.write(str(part)) + content = buf.getvalue() + except Exception: + content = str(c) + # Fallback: LangChain BaseMessage has .type + if role is None and hasattr(m, "type"): + role = str(getattr(m, "type")) + if content is None and hasattr(m, "content"): + content = str(getattr(m, "content")) + plain.append({"role": role or "user", "content": content or ""}) + return plain + + +@register_function(config_type=AgentSpecWorkflowConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]) +async def agent_spec_workflow(config: AgentSpecWorkflowConfig, builder): + # Lazy import to make the dependency optional unless this workflow is used + try: + from langgraph_agentspec_adapter.agentspecloader import AgentSpecLoader # type: ignore + except Exception as e: # pragma: no cover - import error path + raise ImportError( + "Agent Spec adapter not installed. Install with: pip install 'nvidia-nat[agentspec]'" + ) from e + + # Build tool registry from NAT tool names if provided + tools = await builder.get_tools(tool_names=config.tool_names, wrapper_type=LLMFrameworkEnum.LANGCHAIN) + tool_registry = {getattr(t, "name", f"tool_{i}"): t for i, t in enumerate(tools)} if tools else {} + + fmt, payload = read_agentspec_payload(config) + loader = AgentSpecLoader(tool_registry=tool_registry, checkpointer=None, config=None) + + # Compile Agent Spec to a LangGraph component + if fmt == "yaml": + component = loader.load_yaml(payload) + else: + component = loader.load_json(payload) + + async def _response_fn(chat_request_or_message: ChatRequestOrMessage) -> ChatResponse | str: + from nat.agent.base import AGENT_LOG_PREFIX + from langchain_core.messages import trim_messages # lazy import with LANGCHAIN wrapper + + try: + message = GlobalTypeConverter.get().convert(chat_request_or_message, to_type=ChatRequest) + + # Trim message history + trimmed = trim_messages(messages=[m.model_dump() for m in message.messages], + max_tokens=config.max_history, + strategy="last", + token_counter=len, + start_on="human", + include_system=True) + + # Best-effort: pass messages in a generic shape expected by adapter graphs + input_state: dict[str, Any] = {"messages": _to_plain_messages(trimmed)} + + result: Any + result = await component.ainvoke(input_state) + + # Heuristic extraction of assistant content + content: str | None = None + if isinstance(result, dict): + msgs = result.get("messages") + if isinstance(msgs, list) and msgs: + for entry in reversed(msgs): + # LangChain BaseMessage objects have `.type` (e.g., 'ai', 'human') and `.content` + if hasattr(entry, "type") and hasattr(entry, "content"): + role = getattr(entry, "type", None) + if role in ("ai", "assistant", "system"): + content = str(getattr(entry, "content", "")) + break + # Dict-shaped message + if isinstance(entry, dict): + role = entry.get("role") + if role in ("assistant", "system", "ai"): + content = str(entry.get("content", "")) + break + if content is None and "output" in result: + content = str(result.get("output")) + if content is None and isinstance(result, str): + content = result + if content is None: + content = str(result) + + prompt_tokens = sum(len(str(msg.content).split()) for msg in message.messages) + completion_tokens = len(content.split()) if content else 0 + usage = Usage(prompt_tokens=prompt_tokens, completion_tokens=completion_tokens, + total_tokens=prompt_tokens + completion_tokens) + response = ChatResponse.from_string(content, usage=usage) + if chat_request_or_message.is_string: + return GlobalTypeConverter.get().convert(response, to_type=str) + return response + except Exception as ex: # pragma: no cover - surface original exception + logger.error("%s Agent Spec workflow failed: %s", AGENT_LOG_PREFIX, str(ex)) + raise + + yield FunctionInfo.from_fn(_response_fn, description=config.description) diff --git a/src/nat/agent/register.py b/src/nat/agent/register.py index d8173f9c68..097d454db8 100644 --- a/src/nat/agent/register.py +++ b/src/nat/agent/register.py @@ -23,3 +23,4 @@ from .responses_api_agent import register as responses_api_agent from .rewoo_agent import register as rewoo_agent from .tool_calling_agent import register as tool_calling_agent +from .agentspec import register as agentspec diff --git a/tests/conftest.py b/tests/conftest.py index 51bd397313..612c5bfe03 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,7 +53,9 @@ PROJECT_DIR = os.path.dirname(TESTS_DIR) SRC_DIR = os.path.join(PROJECT_DIR, "src") EXAMPLES_DIR = os.path.join(PROJECT_DIR, "examples") -sys.path.append(SRC_DIR) +# Prepend local src so tests run against workspace code rather than any installed package +if SRC_DIR not in sys.path: + sys.path.insert(0, SRC_DIR) os.environ.setdefault("DASK_DISTRIBUTED__WORKER__PYTHON", sys.executable) diff --git a/tests/test_agentspec_config.py b/tests/test_agentspec_config.py new file mode 100644 index 0000000000..79956e2fce --- /dev/null +++ b/tests/test_agentspec_config.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. +# SPDX-License-Identifier: Apache-2.0 + +import pytest + +from nat.agent.agentspec.config import AgentSpecWorkflowConfig + + +def test_agentspec_config_exactly_one_source_yaml(): + cfg = AgentSpecWorkflowConfig(llm_name="dummy", agentspec_yaml="component_type: Agent\nname: test") + assert cfg.agentspec_yaml and not cfg.agentspec_json and not cfg.agentspec_path + + +def test_agentspec_config_exactly_one_source_json(): + cfg = AgentSpecWorkflowConfig(llm_name="dummy", agentspec_json="{}") + assert cfg.agentspec_json and not cfg.agentspec_yaml and not cfg.agentspec_path + + +def test_agentspec_config_exactly_one_source_path(tmp_path): + p = tmp_path / "spec.yaml" + p.write_text("component_type: Agent\nname: test", encoding="utf-8") + cfg = AgentSpecWorkflowConfig(llm_name="dummy", agentspec_path=str(p)) + assert cfg.agentspec_path and not cfg.agentspec_yaml and not cfg.agentspec_json + + +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"agentspec_yaml": "a", "agentspec_json": "{}"}, + {"agentspec_yaml": "a", "agentspec_path": "p"}, + {"agentspec_json": "{}", "agentspec_path": "p"}, + {"agentspec_yaml": "a", "agentspec_json": "{}", "agentspec_path": "p"}, + ], +) +def test_agentspec_config_validation_errors(kwargs): + with pytest.raises(ValueError): + AgentSpecWorkflowConfig(llm_name="dummy", **kwargs) diff --git a/tests/test_agentspec_smoke.py b/tests/test_agentspec_smoke.py new file mode 100644 index 0000000000..8c03c8a025 --- /dev/null +++ b/tests/test_agentspec_smoke.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: Apache-2.0 + +import os +import sys +import pytest + +# Prepend local src to sys.path to ensure local NAT is used +SRC_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "src") +if SRC_DIR not in sys.path: + sys.path.insert(0, SRC_DIR) + + +@pytest.mark.asyncio +async def test_agentspec_adapter_smoke(monkeypatch): + # Minimal Agent Spec defining a simple agent that just echoes user input + spec_yaml = """ +component_type: Agent +name: echo-agent +description: echo +""" + + # Monkeypatch adapter to return a stub runnable that returns the input + class StubComponent: + async def ainvoke(self, value): + if isinstance(value, dict) and "messages" in value: + msgs = value["messages"] + last_user = next((m.get("content") for m in reversed(msgs) if m.get("role") == "user"), "") + return {"output": last_user} + return {"output": str(value)} + + class StubLoader: + def __init__(self, *args, **kwargs): + pass + + def load_yaml(self, _): + return StubComponent() + + import types + + fake_mod = types.ModuleType("langgraph_agentspec_adapter.agentspecloader") + fake_mod.AgentSpecLoader = StubLoader + sys.modules["langgraph_agentspec_adapter"] = types.ModuleType("langgraph_agentspec_adapter") + sys.modules["langgraph_agentspec_adapter.agentspecloader"] = fake_mod + + import nat.agent.agentspec.register # noqa: F401 ensure registration + from nat.agent.agentspec.config import AgentSpecWorkflowConfig + from nat.builder.workflow_builder import WorkflowBuilder + + cfg = AgentSpecWorkflowConfig(llm_name="dummy", agentspec_yaml=spec_yaml) + async with WorkflowBuilder() as builder: + fn = await builder.set_workflow(cfg) + out = await fn.acall_invoke(input_message="hello world") + assert out is not None From 683c5a3a288954a94af39e42006044a2c1caadee Mon Sep 17 00:00:00 2001 From: Yasha Pushak Date: Fri, 9 Jan 2026 15:02:45 -0800 Subject: [PATCH 6/9] Add SPDX headers and docs for Agent Spec Signed-off-by: Yasha Pushak --- docs/source/components/agents/agent-spec.md | 17 +++++++++++++++++ ...agentspec-milestone-1-implementation-plan.md | 17 +++++++++++++++++ scripts/agentspec_smoke.py | 2 ++ tests/test_agentspec_smoke.py | 1 + 4 files changed, 37 insertions(+) diff --git a/docs/source/components/agents/agent-spec.md b/docs/source/components/agents/agent-spec.md index 717af56752..40bb5f46e7 100644 --- a/docs/source/components/agents/agent-spec.md +++ b/docs/source/components/agents/agent-spec.md @@ -1,3 +1,20 @@ + + # Agent Spec Workflow (Milestone 1) This workflow allows running an [Agent Spec] configuration inside NeMo Agent Toolkit by converting it to a LangGraph component via the Agent Spec → LangGraph adapter. diff --git a/docs/source/proposals/agentspec-milestone-1-implementation-plan.md b/docs/source/proposals/agentspec-milestone-1-implementation-plan.md index 35610164db..361a694dae 100644 --- a/docs/source/proposals/agentspec-milestone-1-implementation-plan.md +++ b/docs/source/proposals/agentspec-milestone-1-implementation-plan.md @@ -1,3 +1,20 @@ + + # Milestone 1: Agent Spec Import Support in NeMo Agent Toolkit Author: Oracle (Yasha Pushak) • Reviewer: NVIDIA (NeMo team) diff --git a/scripts/agentspec_smoke.py b/scripts/agentspec_smoke.py index 4c021b15b1..8aaf0024f2 100644 --- a/scripts/agentspec_smoke.py +++ b/scripts/agentspec_smoke.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 import asyncio import sys import types diff --git a/tests/test_agentspec_smoke.py b/tests/test_agentspec_smoke.py index 8c03c8a025..af308714d3 100644 --- a/tests/test_agentspec_smoke.py +++ b/tests/test_agentspec_smoke.py @@ -1,3 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import os From 3926ef13cfc7a6e38903096327ee23f239996248 Mon Sep 17 00:00:00 2001 From: Yasha Pushak Date: Mon, 12 Jan 2026 14:08:55 -0800 Subject: [PATCH 7/9] Fixed license headers Signed-off-by: Yasha Pushak --- docs/source/components/agents/agent-spec.md | 6 +++--- scripts/agentspec_smoke.py | 15 +++++++++++++-- src/nat/agent/agentspec/config.py | 15 +++++++++++++-- src/nat/agent/agentspec/register.py | 14 +++++++++++++- tests/test_agentspec_config.py | 14 +++++++++++++- tests/test_agentspec_smoke.py | 14 +++++++++++++- 6 files changed, 68 insertions(+), 10 deletions(-) diff --git a/docs/source/components/agents/agent-spec.md b/docs/source/components/agents/agent-spec.md index 40bb5f46e7..15872be244 100644 --- a/docs/source/components/agents/agent-spec.md +++ b/docs/source/components/agents/agent-spec.md @@ -15,7 +15,7 @@ limitations under the License. --> -# Agent Spec Workflow (Milestone 1) +# Agent Spec Workflow This workflow allows running an [Agent Spec] configuration inside NeMo Agent Toolkit by converting it to a LangGraph component via the Agent Spec → LangGraph adapter. @@ -45,7 +45,7 @@ Exactly one of `agentspec_yaml`, `agentspec_json`, or `agentspec_path` must be p - Tools: NAT tools provided in `tool_names` are exposed to the adapter `tool_registry` by name. If the Agent Spec also defines tools, the registries are merged; duplicate names are overwritten by NAT tools. - I/O: Inputs are standard `ChatRequest` messages; the workflow returns a `ChatResponse`. -- Streaming: Non‑streaming response in M1. -- Checkpointing: Not wired in M1. +- Streaming: Non supported. +- Checkpointing: Not supported. [Agent Spec]: https://github.com/oracle/agent-spec diff --git a/scripts/agentspec_smoke.py b/scripts/agentspec_smoke.py index 8aaf0024f2..85ffff090d 100644 --- a/scripts/agentspec_smoke.py +++ b/scripts/agentspec_smoke.py @@ -1,6 +1,17 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) , NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import asyncio import sys import types diff --git a/src/nat/agent/agentspec/config.py b/src/nat/agent/agentspec/config.py index 9a0448b728..b98076971b 100644 --- a/src/nat/agent/agentspec/config.py +++ b/src/nat/agent/agentspec/config.py @@ -1,6 +1,17 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) , NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 - +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from pathlib import Path from pydantic import Field, model_validator diff --git a/src/nat/agent/agentspec/register.py b/src/nat/agent/agentspec/register.py index e8c20311f0..f46e1ae15b 100644 --- a/src/nat/agent/agentspec/register.py +++ b/src/nat/agent/agentspec/register.py @@ -1,5 +1,17 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) , NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import io import logging diff --git a/tests/test_agentspec_config.py b/tests/test_agentspec_config.py index 79956e2fce..204c62b93e 100644 --- a/tests/test_agentspec_config.py +++ b/tests/test_agentspec_config.py @@ -1,5 +1,17 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) , NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import pytest diff --git a/tests/test_agentspec_smoke.py b/tests/test_agentspec_smoke.py index af308714d3..b10056e612 100644 --- a/tests/test_agentspec_smoke.py +++ b/tests/test_agentspec_smoke.py @@ -1,5 +1,17 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) , NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import os import sys From 5c09bd09d618c8f94ddcf30be85a684f7d2e1687 Mon Sep 17 00:00:00 2001 From: Yasha Pushak Date: Mon, 12 Jan 2026 14:11:44 -0800 Subject: [PATCH 8/9] Removed unneeded file Signed-off-by: Yasha Pushak --- ...entspec-milestone-1-implementation-plan.md | 189 ------------------ 1 file changed, 189 deletions(-) delete mode 100644 docs/source/proposals/agentspec-milestone-1-implementation-plan.md diff --git a/docs/source/proposals/agentspec-milestone-1-implementation-plan.md b/docs/source/proposals/agentspec-milestone-1-implementation-plan.md deleted file mode 100644 index 361a694dae..0000000000 --- a/docs/source/proposals/agentspec-milestone-1-implementation-plan.md +++ /dev/null @@ -1,189 +0,0 @@ - - -# Milestone 1: Agent Spec Import Support in NeMo Agent Toolkit - -Author: Oracle (Yasha Pushak) • Reviewer: NVIDIA (NeMo team) - -Status: Draft (Implementation Plan) - -## Scope - -Add a new workflow/function type to NeMo Agent Toolkit that executes Agent Spec configurations using the Agent Spec → LangGraph adapter. This enables representing richer agentic patterns (flows, branches/loops, multi‑agent) inside NeMo, while reusing NeMo’s tooling (evaluation, profiling, observability). - -Non-goals for this milestone: -- Multi-runtime selection (CrewAI, WayFlow, …) — planned for Milestone 2. -- Sub-component tuning via Agent Spec graph introspection — planned for Milestone 3. -- New UI or API surface beyond what is required to run an Agent Spec workflow. - -## High-level Design - -- Introduce a new function config type: `AgentSpecWorkflowConfig` with `_type: agent_spec`. -- At build time, use the Agent Spec LangGraph adapter to convert an Agent Spec document into a `langgraph` CompiledStateGraph and run it as the workflow implementation. -- Reuse NeMo’s Builder to construct a tool registry compatible with LangGraph/LangChain tools from NAT-configured tools. -- Keep inputs/outputs aligned with NeMo’s `ChatRequest`/`ChatResponse` for consistent UX. -- Hook into NeMo profiling/observability using existing `LLMFrameworkEnum.LANGCHAIN` wrapper integration. - -## Key Integration Points - -- Registration: `@register_function(config_type=AgentSpecWorkflowConfig, framework_wrappers=[LLMFrameworkEnum.LANGCHAIN])`. -- Config model: subclass `AgentBaseConfig` (for parity with `react_agent`), add fields to accept the Agent Spec payload and minimal runtime options. -- Adapter usage: `langgraph_agentspec_adapter.AgentSpecLoader` to `load_yaml`/`load_json` or from a structured Pydantic field converted to YAML/JSON. -- Tools: leverage `builder.get_tools(..., wrapper_type=LLMFrameworkEnum.LANGCHAIN)` to obtain LangChain-compatible tools, then pass them into the adapter `tool_registry` by name. -- Execution: compile LangGraph component from Agent Spec and `ainvoke` based on `ChatRequest` messages; return `ChatResponse` with basic usage stats, consistent with existing agents. - -## Configuration Schema (initial) - -Example NAT YAML: - -```yaml -workflow: - _type: agent_spec - description: Agent Spec workflow - # Exactly one of the following should be provided - agentspec_yaml: | - component_type: Agent - id: bdd2369b-82e6-488f-be2c-44f05b244cab - name: writing agent - description: Agent to help write blog articles - system_prompt: "You're a helpful writing assistant..." - inputs: - - title: user_name - type: string - llm_config: - component_type: VllmConfig - model_id: gpt-oss-120b - url: http://url.to.my.vllm.server/ - tools: - - component_type: ServerTool - name: pretty_formatting - description: Format paragraph spacing and indentation - inputs: - - title: paragraph - type: string - outputs: - - title: formatted_paragraph - type: string - # Optional alternatives to inline YAML - # agentspec_json: "{...}" - # agentspec_path: path/to/agent_spec.yaml - tool_names: [pretty_formatting] # map to NeMo/LC tools where applicable - verbose: true - max_history: 15 -``` - -Notes: -- We support exactly one of: `agentspec_yaml`, `agentspec_json`, or `agentspec_path`. -- `tool_names` are optional. If provided, we populate the adapter `tool_registry` with LC-compatible wrappers for those names. If the Agent Spec includes ServerTools with embedded `func`, both can coexist; the registry complements the spec. -- We reuse standard agent flags (`verbose`, `max_history`, `log_response_max_chars`, etc.) when easy; reserved flags beyond minimal needs can be deferred. - -## Detailed Tasks - -1) Data model and registration -- Add `AgentSpecWorkflowConfig` in `nat/agent/agentspec/register.py`: - - Base: `AgentBaseConfig`. - - Name: `agent_spec`. - - Fields: - - `description: str = "Agent Spec Workflow"`. - - `agentspec_yaml: str | None`. - - `agentspec_json: str | None`. - - `agentspec_path: str | None`. - - `tool_names: list[FunctionRef | FunctionGroupRef] = []` (optional). - - `max_history: int = 15`, `verbose: bool = False`, `log_response_max_chars: int | None = None`. - - Validation: exactly one of `agentspec_yaml/json/path` must be set. -- Register build function with `@register_function(..., framework_wrappers=[LLMFrameworkEnum.LANGCHAIN])`. - -2) Builder integration (execution function) -- Resolve the Agent Spec payload (from YAML/JSON/path) into a string. -- Build LC-compatible tools via `builder.get_tools(tool_names=..., wrapper_type=LLMFrameworkEnum.LANGCHAIN)` and create a dict `{name: tool}` to pass as `tool_registry` to `AgentSpecLoader`. -- Instantiate adapter: `AgentSpecLoader(tool_registry=..., checkpointer=None, config=None)`. -- Load to LangGraph component via `load_yaml`/`load_json`. -- Implement NAT function body accepting `ChatRequestOrMessage`: - - Convert to `ChatRequest`. - - Trim/limit history based on `max_history` (reuse logic from `react_agent`). - - Prepare input state/messages as required by the compiled graph: - - If the compiled component expects `{"messages": [...]}`, pass aligned payload. - - Otherwise, pass the minimal input the adapter expects; start with messages-only path as per adapter examples. - - `await graph.ainvoke(state, config={"recursion_limit": safe_default})`. - - Extract final message content and build `ChatResponse` with basic token usage. - -3) Dependency management -- Runtime dependency on `langgraph-agentspec-adapter` and `pyagentspec`. -- Add an extra in NeMo toolkit packaging (e.g., `agentspec`) and document installation: `pip install nat[agentspec]`. -- For source builds in this mono-repo, ensure import path resolution works during tests (use dev requirements or local path instruction in docs). - -4) Error handling and validation -- Clear errors for: - - Missing/invalid Agent Spec payload. - - Both/none of YAML/JSON/path provided. - - Tool name not found in NAT registry when specified. - - Adapter conversion/type errors. -- Log with existing agent log prefix; respect `verbose` and `log_response_max_chars` where applicable. - -5) Observability/profiling -- Mark function with `framework_wrappers=[LLMFrameworkEnum.LANGCHAIN]` to enable existing profiler hooks. -- Ensure intermediate steps respect NAT callback/logging conventions where feasible in this milestone (best effort; deep step-by-step surfacing can be deferred). - -6) Documentation -- Add a new page under `docs/source/components/agents/agent-spec.md`: - - Purpose and capabilities. - - Installation instructions (extra deps). - - Minimal YAML example and tool mapping guidance. - - Notes/limitations in Milestone 1. -- Update tutorials index and examples list. - -7) Testing -- Unit tests: - - Config validation (exactly-one-of source fields). - - Tool registry mapping (with and without `tool_names`). -- Adapter smoke test: - - Use a tiny Agent Spec with a single tool or echo flow; verify invocation returns a `ChatResponse`. - - Mark network/LLM usage as skipped unless a local model is configured; prefer a spec that does not require external LLM for the smoke test (e.g., a simple tool graph), or mock the adapter/LLM call in tests. - -## Open Questions / Assumptions - -- Tool mapping precedence: if an Agent Spec defines ServerTools and NAT also provides `tool_names`, we will merge, with NAT tools overwriting duplicate names in the adapter registry (document this behavior). -- Input schema: In Milestone 1 we standardize on `ChatRequest` messages as input; richer parameter passing (matching Agent Spec inputs) can be extended later. -- Checkpointing: adapter supports a `Checkpointer`; we defer integration to a later milestone unless straightforward to pass through. -- Streaming: we target non-streaming response first; streaming support can be added post-M1 if needed. - -## Risks and Mitigations - -- Dependency drift between LangGraph/LangChain versions: pin compatible versions in the `agentspec` extra and CI. -- Mismatch between adapter’s expected state shape and our message wrapper: start with the adapter’s documented pattern using `{"messages": [...]}` and add a thin translator if necessary. -- Tool contract mismatch: validate tool signatures with simple probes and surface clear errors. - -## Delivery Checklist - -- [x] `AgentSpecWorkflowConfig` added and registered. -- [x] Build function executes adapter graph and returns `ChatResponse`. -- [x] Packaging: `agentspec` extra with pinned dependencies and docs stub. -- [ ] Docs page with examples and limitations. -- [x] Unit tests: config validation. -- [ ] Smoke test with minimal Agent Spec graph. - -## Plan Updates - -- Added optional dependency extra `agentspec` in `pyproject.toml` with `pyagentspec` and `langgraph-agentspec-adapter`. -- Implemented `AgentSpecWorkflowConfig` at `src/nat/agent/agentspec/register.py:1` and wired registration in `src/nat/agent/register.py:1`. -- Added basic config validation tests in `tests/test_agentspec_config.py:1`. - -## Appendix: References - -- NAT registration pattern: `nat/agent/react_agent/register.py` (`ReActAgentWorkflowConfig`, `react_agent_workflow`). -- Type registry and wrappers: `nat/cli/register_workflow.py`, `LLMFrameworkEnum.LANGCHAIN`. -- Adapter entrypoints: `langgraph_agentspec_adapter.AgentSpecLoader` (load_yaml/json/component). From 369634e546eaabb239776af1be0319452e980e48 Mon Sep 17 00:00:00 2001 From: Yasha Pushak Date: Tue, 13 Jan 2026 12:52:18 -0800 Subject: [PATCH 9/9] Conformance with checks --- docs/source/components/agents/agent-spec.md | 5 +++-- scripts/agentspec_smoke.py | 4 +++- src/nat/agent/agentspec/config.py | 13 +++++++------ src/nat/agent/agentspec/register.py | 21 +++++++++++++-------- tests/test_agentspec_config.py | 18 +++++++++++++----- tests/test_agentspec_smoke.py | 5 ++++- 6 files changed, 43 insertions(+), 23 deletions(-) diff --git a/docs/source/components/agents/agent-spec.md b/docs/source/components/agents/agent-spec.md index 15872be244..cfebcec3a6 100644 --- a/docs/source/components/agents/agent-spec.md +++ b/docs/source/components/agents/agent-spec.md @@ -1,5 +1,5 @@ agentspec_path: path/to/agent_spec.yaml # or agentspec_yaml / agentspec_json tool_names: [pretty_formatting] max_history: 15 @@ -43,7 +44,7 @@ Exactly one of `agentspec_yaml`, `agentspec_json`, or `agentspec_path` must be p ## Notes and limitations -- Tools: NAT tools provided in `tool_names` are exposed to the adapter `tool_registry` by name. If the Agent Spec also defines tools, the registries are merged; duplicate names are overwritten by NAT tools. +- Tools: NeMo Agent toolkit built-in tools provided in `tool_names` are exposed to the adapter `tool_registry` by name. If the Agent Spec also defines tools, the registries are merged; duplicate names are overwritten by built-in tools. - I/O: Inputs are standard `ChatRequest` messages; the workflow returns a `ChatResponse`. - Streaming: Non supported. - Checkpointing: Not supported. diff --git a/scripts/agentspec_smoke.py b/scripts/agentspec_smoke.py index 85ffff090d..4f169f975e 100644 --- a/scripts/agentspec_smoke.py +++ b/scripts/agentspec_smoke.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) , NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,6 +30,7 @@ async def main(): # Create a fake adapter module that returns a stub component class StubComponent: + async def ainvoke(self, value): if isinstance(value, dict) and "messages" in value: msgs = value["messages"] @@ -38,6 +39,7 @@ async def ainvoke(self, value): return {"output": str(value)} class StubLoader: + def __init__(self, *args, **kwargs): pass diff --git a/src/nat/agent/agentspec/config.py b/src/nat/agent/agentspec/config.py index b98076971b..8c8117c67d 100644 --- a/src/nat/agent/agentspec/config.py +++ b/src/nat/agent/agentspec/config.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) , NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,10 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. from pathlib import Path -from pydantic import Field, model_validator + +from pydantic import Field +from pydantic import model_validator from nat.data_models.agent import AgentBaseConfig -from nat.data_models.component_ref import FunctionGroupRef, FunctionRef +from nat.data_models.component_ref import FunctionGroupRef +from nat.data_models.component_ref import FunctionRef class AgentSpecWorkflowConfig(AgentBaseConfig, name="agent_spec"): @@ -34,8 +37,7 @@ class AgentSpecWorkflowConfig(AgentBaseConfig, name="agent_spec"): agentspec_path: str | None = Field(default=None, description="Path to an Agent Spec YAML/JSON file") tool_names: list[FunctionRef | FunctionGroupRef] = Field( - default_factory=list, description="Optional list of tool names/groups to expose to the Agent Spec runtime." - ) + default_factory=list, description="Optional list of tool names/groups to expose to the Agent Spec runtime.") max_history: int = Field(default=15, description="Maximum number of messages to keep in conversation history.") @@ -60,4 +62,3 @@ def read_agentspec_payload(config: AgentSpecWorkflowConfig) -> tuple[str, str]: ext = path.suffix.lower() fmt = "json" if ext == ".json" else "yaml" return (fmt, text) - diff --git a/src/nat/agent/agentspec/register.py b/src/nat/agent/agentspec/register.py index f46e1ae15b..adaf337c47 100644 --- a/src/nat/agent/agentspec/register.py +++ b/src/nat/agent/agentspec/register.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) , NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,9 +20,14 @@ from nat.builder.framework_enum import LLMFrameworkEnum from nat.builder.function_info import FunctionInfo from nat.cli.register_workflow import register_function -from nat.data_models.api_server import ChatRequest, ChatRequestOrMessage, ChatResponse, Message, Usage +from nat.data_models.api_server import ChatRequest +from nat.data_models.api_server import ChatRequestOrMessage +from nat.data_models.api_server import ChatResponse +from nat.data_models.api_server import Usage from nat.utils.type_converter import GlobalTypeConverter -from .config import AgentSpecWorkflowConfig, read_agentspec_payload + +from .config import AgentSpecWorkflowConfig +from .config import read_agentspec_payload logger = logging.getLogger(__name__) @@ -71,9 +76,7 @@ async def agent_spec_workflow(config: AgentSpecWorkflowConfig, builder): try: from langgraph_agentspec_adapter.agentspecloader import AgentSpecLoader # type: ignore except Exception as e: # pragma: no cover - import error path - raise ImportError( - "Agent Spec adapter not installed. Install with: pip install 'nvidia-nat[agentspec]'" - ) from e + raise ImportError("Agent Spec adapter not installed. Install with: pip install 'nvidia-nat[agentspec]'") from e # Build tool registry from NAT tool names if provided tools = await builder.get_tools(tool_names=config.tool_names, wrapper_type=LLMFrameworkEnum.LANGCHAIN) @@ -89,9 +92,10 @@ async def agent_spec_workflow(config: AgentSpecWorkflowConfig, builder): component = loader.load_json(payload) async def _response_fn(chat_request_or_message: ChatRequestOrMessage) -> ChatResponse | str: - from nat.agent.base import AGENT_LOG_PREFIX from langchain_core.messages import trim_messages # lazy import with LANGCHAIN wrapper + from nat.agent.base import AGENT_LOG_PREFIX + try: message = GlobalTypeConverter.get().convert(chat_request_or_message, to_type=ChatRequest) @@ -136,7 +140,8 @@ async def _response_fn(chat_request_or_message: ChatRequestOrMessage) -> ChatRes prompt_tokens = sum(len(str(msg.content).split()) for msg in message.messages) completion_tokens = len(content.split()) if content else 0 - usage = Usage(prompt_tokens=prompt_tokens, completion_tokens=completion_tokens, + usage = Usage(prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, total_tokens=prompt_tokens + completion_tokens) response = ChatResponse.from_string(content, usage=usage) if chat_request_or_message.is_string: diff --git a/tests/test_agentspec_config.py b/tests/test_agentspec_config.py index 204c62b93e..940f887b50 100644 --- a/tests/test_agentspec_config.py +++ b/tests/test_agentspec_config.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) , NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,10 +39,18 @@ def test_agentspec_config_exactly_one_source_path(tmp_path): "kwargs", [ {}, - {"agentspec_yaml": "a", "agentspec_json": "{}"}, - {"agentspec_yaml": "a", "agentspec_path": "p"}, - {"agentspec_json": "{}", "agentspec_path": "p"}, - {"agentspec_yaml": "a", "agentspec_json": "{}", "agentspec_path": "p"}, + { + "agentspec_yaml": "a", "agentspec_json": "{}" + }, + { + "agentspec_yaml": "a", "agentspec_path": "p" + }, + { + "agentspec_json": "{}", "agentspec_path": "p" + }, + { + "agentspec_yaml": "a", "agentspec_json": "{}", "agentspec_path": "p" + }, ], ) def test_agentspec_config_validation_errors(kwargs): diff --git a/tests/test_agentspec_smoke.py b/tests/test_agentspec_smoke.py index b10056e612..5b5ff7d238 100644 --- a/tests/test_agentspec_smoke.py +++ b/tests/test_agentspec_smoke.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) , NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,7 @@ import os import sys + import pytest # Prepend local src to sys.path to ensure local NAT is used @@ -34,6 +35,7 @@ async def test_agentspec_adapter_smoke(monkeypatch): # Monkeypatch adapter to return a stub runnable that returns the input class StubComponent: + async def ainvoke(self, value): if isinstance(value, dict) and "messages" in value: msgs = value["messages"] @@ -42,6 +44,7 @@ async def ainvoke(self, value): return {"output": str(value)} class StubLoader: + def __init__(self, *args, **kwargs): pass