NVIDIA / NVIDIA/NeMo-Agent-Toolkit

DecomposedType.is_instance fails for union types, so functions with an optional single input cannot be invoked with a value

Open Beginner friendly
#2,224 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

Version

develop (013255a), also reproduces on 1.9.0

Which installation method(s) does this occur on?

Source, PyPI

Describe the bug.

DecomposedType.is_instance (packages/nvidia_nat_core/src/nat/utils/type_utils.py, around line 380) takes the union's root type and passes it to isinstance. For str | None the root is types.UnionType, so the check is always False; for typing.Optional[str] the root is typing.Union, and isinstance raises TypeError: typing.Union cannot be used with isinstance().

is_instance is what Function._convert_input, _needs_conversion and TypeConverter use to decide whether a value already matches the declared input type, so a function whose single input is annotated str | None or Optional[str] cannot be invoked with a plain value at all. Passing None works, passing "hi" fails. The same happens when requesting an optional output via to_type.

Minimum reproducible example
import asyncio
import typing

from nat.utils.type_utils import DecomposedType

print(DecomposedType(str | None).is_instance("hi"))  # False, expected True
print(DecomposedType(typing.Optional[str]).is_instance("hi"))  # TypeError, expected True

# End to end, through a registered function
from nat.builder.builder import Builder
from nat.builder.workflow_builder import WorkflowBuilder
from nat.cli.register_workflow import register_function
from nat.data_models.function import FunctionBaseConfig


class DummyConfig(FunctionBaseConfig, name="dummy_optional"):
    pass


@register_function(config_type=DummyConfig)
async def _register(config: DummyConfig, b: Builder):

    async def _inner(message: str | None) -> str:
        return f"got {message!r}"

    yield _inner


async def main():
    async with WorkflowBuilder() as builder:
        fn = await builder.add_function(name="f", config=DummyConfig())
        print(await fn.ainvoke(None))  # got None
        print(await fn.ainvoke("hi"))  # TypeError, expected "got 'hi'"


asyncio.run(main())
Relevant log output
TypeError: typing.Union cannot be used with isinstance()

(for the str | None spelling the failure surfaces as a conversion TypeError from Function.ainvoke because is_instance returned False and no converter exists)

Other/Misc.

Proposed fix: when the base type is a union, check the instance against each member (any(DecomposedType(arg).is_instance(instance) for arg in args)), which also handles NoneType and typing.Any members. I have this implemented with unit tests and will open a PR. Report prepared with AI assistance (Claude Code); reproduced locally on develop.

Code of Conduct
  • I agree to follow this project's Code of Conduct

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/utils/type_utils.py around DecomposedType.is_instance, then trace its use from Function._convert_input, _needs_conversion, and TypeConverter. Reproduce the str | None and typing.Optional[str] cases from the issue, and run or extend the unit tests so plain values and None are accepted for optional inputs and outputs.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.