NVIDIA / NVIDIA/NeMo-Agent-Toolkit
Defense middlewares: PII / content safety / output verifier analyze structured stream chunks as pydantic reprs
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 2.6k
- Forks
- 762
- Avg merge
- 21h 28m
- Merged PRs (30d)
- 27
Description
Version
1.9.0 (nvidia-nat-security==1.9.0), also present on develop at 013255a.
Which installation method(s) does this occur on?
- PyPi
- Source
Describe the bug.
The three stream-buffering defense middlewares serialize each buffered chunk with str(chunk) before analysis, so typed chunks (for example ChatResponseChunk, which the toolkit's own react_agent yields) are analyzed, length-checked, and anonymized as pydantic repr dumps instead of the generated reply text:
OutputVerifierMiddleware(defense_middleware_output_verifier.py:406) sends the repr to the verifier LLM.ContentSafetyGuardMiddleware(defense_middleware_content_guard.py:422) sends the repr to the guard model and counts the repr length againstmax_content_length. Reprs are far longer than the text, so streams that fit the configured budget can stop early withinput length ... exceeds configured max_content_length.PIIDefenseMiddleware(defense_middleware_pii.py:311) runs PII detection/anonymization over the repr.
Today, str() on a streamed chunk produces:
id='echo' choices=[ChatResponseChunkChoice(finish_reason=None, index=0, delta=ChoiceDelta(content='Hello world', role=None, tool_calls=None))] created=datetime.datetime(2026, 9, 19, ..., tzinfo=datetime.timezone.utc) model='unknown-model' object='chat.completion.chunk' ...
Expected: the analysis input is the chunk's text content (Hello world), consistent with how Function.astream(to_type=str) renders streamed output through GlobalTypeConverter.
This is the same defect class as #2230 (guardrails middleware, addressed for that middleware by #2234) in the defense middlewares. A fix with one regression test per middleware is ready and will be linked here.
Minimum reproducible example
from nat.data_models.api_server import ChatResponseChunk
chunk = ChatResponseChunk.create_streaming_chunk("Hello world", id_="echo")
print(str(chunk)) # what the middlewares analyze today
print(chunk.choices[0].delta.content) # what they should analyze
End-to-end, function_middleware_stream with action="refusal" over a stream of
ChatResponseChunks feeds str(chunk) into the analyzer/guard/verifier; the regression
tests in the linked PR drive all three middlewares that way with mocked analyzers.
Relevant log output
call(text="id='echo' choices=[ChatResponseChunkChoice(finish_reason=None, index=0, delta=ChoiceDelta(content='Hello world', role=None, tool_calls=None))] created=datetime.datetime(...) model='unknown-model' object='chat.completion.chunk' ...")
Other/Misc.
Fix plan: one shared helper (stream_chunk_to_text) that returns strings unchanged, renders toolkit data models through GlobalTypeConverter (the same conversion the framework uses for to_type=str), and keeps the previous str() rendering for values without a text representation. Used by all three middlewares, with a regression test per middleware.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the buffering paths in defense_middleware_output_verifier.py:406, defense_middleware_content_guard.py:422, and defense_middleware_pii.py:311, then inspect GlobalTypeConverter and function_middleware_stream. Add regression coverage for all three middlewares using ChatResponseChunk streams, and verify that analysis receives the chunk text rather than its repr while non-text values retain the previous behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100