NVIDIA / NVIDIA/NeMo-Agent-Toolkit

A NAT function cannot take zero arguments: FunctionInfo.from_fn rejects no-input tools

Open
#2,184 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug Needs Triage
Dominant language
Python
Stars
2.6k
Forks
762
Avg merge
21h 28m
Merged PRs (30d)
27

Description

Version

1.8.0 (also on develop @ 2d2e342)

Which installation method(s) does this occur on?

PyPi

Describe the bug.

FunctionInfo.from_fn cannot register a function that takes no parameters. A zero-argument
function is recognised when the descriptor is built, but never wrapped, so it reaches a
validator that requires exactly one parameter and raises.

FunctionDescriptor.from_function handles the zero case explicitly
(packages/nvidia_nat_core/src/nat/builder/function_info.py, develop @ 2d2e342):

arg_count = len(sig.parameters)

if (arg_count == 0):
    input_type = NoneType
    is_input_typed = False
    input_schema = NoneType
elif (arg_count == 1):
    ...

But FunctionInfo.create only wraps the multi-argument case:

if (final_single_fn_desc.arg_count > 1):
    if (input_schema is not None):
        logger.warning("Using provided input_schema for multi-argument function")
    else:
        input_schema = final_single_fn_desc.get_base_model_function_input()
    ...
    final_single_fn = _convert_input_pydantic

Functions with more than one parameter are therefore collapsed into a single-parameter
function whose input is a generated Pydantic model. There is no corresponding
arg_count == 0 branch, so a zero-argument function passes through unchanged and hits
_validate_single_fn:

if len(sig.parameters) != 1:
    raise ValueError("single_fn must have exactly one parameter")

The result is that 1 and 3 parameters both work while 0 does not, which reads as an
oversight rather than an intentional constraint - the zero case is recognised at the
descriptor level and then not carried through.

No-input tools are common - fetch the current policy, get the current time, list everything,
health check. The workaround is to add a dummy parameter the tool ignores. That parameter is
then advertised to the model, which has to invent a value for it, so the workaround is
visible in the model's tool schema rather than hidden in the adapter.

Expected: a zero-argument function registers, with an empty input schema.

Suggested approach: mirror the existing arg_count > 1 handling with an arg_count == 0
branch that wraps the function in a single-parameter shim taking an empty Pydantic model, so
the rest of the pipeline continues to see exactly one input. The decision worth making
explicitly is what that empty schema should look like on the wire for the framework adapters
that read it - which is why this is filed as an issue rather than a PR.

Sketch of that branch, mirroring the existing multi-argument one in FunctionInfo.create
(develop @ 2d2e342). Not tested - the empty-schema question below should be settled first:

if (final_single_fn_desc.arg_count == 0):
    if (input_schema is None):
        # An input model with no fields, so the rest of the pipeline still
        # sees exactly one parameter.
        input_schema = create_model(f"{final_single_fn.__name__}Input")

    saved_final_single_fn = final_single_fn

    async def _discard_empty_input(value: input_schema) -> final_single_fn_desc.output_type:
        # Nothing to unpack - the function takes no arguments.
        return await saved_final_single_fn()

    final_single_fn = _discard_empty_input

    # Reset the descriptor
    final_single_fn_desc = FunctionDescriptor.from_function(final_single_fn)

elif (final_single_fn_desc.arg_count > 1):
    ...  # unchanged

The same treatment would be needed on the streaming path, which has the matching
final_stream_fn_desc.arg_count > 1 branch and no zero case either.

The part that is genuinely a maintainer decision, and the reason this is an issue rather than
a PR: what an empty input schema should look like on the wire. A field-less Pydantic model
serialises to {"type": "object", "properties": {}}, which the framework adapters then hand
to their own tool-declaration builders. Whether every adapter is happy with an empty
properties object - or whether a no-input tool should be represented some other way - is a
call about NAT's public contract, not something worth guessing at from outside.

Minimum reproducible example
import asyncio
import inspect

from nat.builder.function_info import FunctionInfo


async def no_args() -> str:
    """A tool that needs no input, e.g. fetch the current policy."""
    return "policy text"


async def one_arg(text: str) -> str:
    """One parameter."""
    return text


async def three_args(query: str, limit: int = 5, region: str = "global") -> str:
    """Three parameters, two optional."""
    return f"{query}{limit}{region}"


async def main():
    for fn in (no_args, one_arg, three_args):
        count = len(inspect.signature(fn).parameters)
        try:
            FunctionInfo.from_fn(fn, description=fn.__doc__)
            print(f"{fn.__name__:<12} ({count} params): OK")
        except Exception as exc:
            print(f"{fn.__name__:<12} ({count} params): {type(exc).__name__}: {exc}")


asyncio.run(main())
Relevant log output
no_args      (0 params): ValueError: single_fn must have exactly one parameter
one_arg      (1 params): OK
three_args   (3 params): OK
Other/Misc.

Traceback for the failing case:

  File ".../nat/builder/function_info.py", line 621, in from_fn
    return FunctionInfo.create(single_fn=single_fn, ...)
  File ".../nat/builder/function_info.py", line 543, in create
    return FunctionInfo(single_fn=final_single_fn, ...)
  File ".../nat/builder/function_info.py", line 311, in __init__
    single_input_type, single_output_type = _validate_single_fn(single_fn)
  File ".../nat/builder/function_info.py", line 61, in _validate_single_fn
    raise ValueError("single_fn must have exactly one parameter")
ValueError: single_fn must have exactly one parameter

Environment:

nvidia-nat-core 1.8.0
python          3.11.9
OS              Windows 11
Code of Conduct
  • I agree to follow the NeMo Agent Toolkit Code of Conduct
  • I have searched the open bugs and have found no duplicates for this bug report

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/nvidia_nat_core/src/nat/builder/function_info.py, especially FunctionInfo.create, FunctionDescriptor.from_function, and _validate_single_fn. Compare the existing multi-argument handling with the matching streaming path, then use the provided no_args reproduction to trace how the input schema reaches framework adapters. Done means zero-argument functions register successfully with an agreed empty input schema on both paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend-api-design
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.