Skip to content

feat(server): allow tool calls outside passthrough mode without dialog flows - #2188

Open
christinaexyou wants to merge 2 commits into
NVIDIA-NeMo:pouyanpi/improve-generate-colangfrom
christinaexyou:pouyanpi/improve-generate-colang
Open

feat(server): allow tool calls outside passthrough mode without dialog flows#2188
christinaexyou wants to merge 2 commits into
NVIDIA-NeMo:pouyanpi/improve-generate-colangfrom
christinaexyou:pouyanpi/improve-generate-colang

Conversation

@christinaexyou

Copy link
Copy Markdown
Contributor

… flows

Description

Tool calls were previously gated on passthrough mode. They are now allowed
in any config as long as no dialog flows are defined (user_messages or
single_call dialog rails). Streaming requests still reject tools/tool_choice/
parallel_tool_calls. We explicitly reject tool call requests if dialog flows or streaming is enabled in the config.

Related Issue(s)

Verification

AI Assistance

  • No AI tools were used.
  • AI tools were used; a human reviewed and can explain every change (tool: ___).

Checklist

  • I've read the CONTRIBUTING guidelines.
  • This PR links to a triaged issue assigned to me.
  • My PR title follows the project commit convention.
  • I've updated the documentation if applicable.
  • I've added tests if applicable.
  • I've noted any verification beyond CI and any checks I couldn't run.
  • I did not update generated changelog files manually.
  • I addressed all CodeRabbit, Greptile, and other review comments, or replied with why no change is needed.
  • @mentions of the person or team responsible for reviewing proposed changes.

@github-actions github-actions Bot added status: needs triage New issues that have not yet been reviewed or categorized. size: L needs: signing labels Jul 17, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

PR merge guidance

@christinaexyou thanks for the PR. GitHub is currently blocking merge for one or more repository requirements:

  • 1 commit does not have a verified signature (ee0bf26). Please sign the commits and force-push the updated branch.

Relevant guide:

… flows

Signed-off-by: Christina Xu <chrxu@redhat.com>
@christinaexyou
christinaexyou force-pushed the pouyanpi/improve-generate-colang branch from aff6a45 to 9c8f260 Compare July 17, 2026 17:44
@Pouyanpi Pouyanpi changed the title fix(server): allow tool calls outside passthrough mode without dialog… feat(server): allow tool calls outside passthrough mode without dialog flows Jul 20, 2026
@Pouyanpi Pouyanpi added status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile). and removed status: needs triage New issues that have not yet been reviewed or categorized. labels Jul 21, 2026
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR removes the passthrough requirement for tool calls: BotToolCalls events now surface from any config that reaches _emit_general_bot_turn (i.e., any config without dialog flows). The server guard is split into a streaming rejection and a dialog-flow rejection, and all tests patching get_and_clear_tool_calls_contextvar are updated to the new import location in generation.

  • generation.py: Removes the if self.config.passthrough: gate around get_and_clear_tool_calls_contextvar(), so tool calls surface unconditionally for the general bot-turn path.
  • api.py: Replaces a single passthrough+streaming check with two explicit guards — one for streaming and one for dialog-flow configs (user_messages or single_call.enabled). The single_call.enabled condition is overly broad: configs with single_call.enabled=True but no user_messages still route through _emit_general_bot_turn and would surface tool calls correctly, yet the server now blocks them with a 422.
  • Tests: Broad coverage is added, but test_tool_calls_fail_in_dialog_flows_non_passthrough uses an unrecognized YAML key (dialog:) and never populates tool_calls_var, so it does not actually verify dialog-flow suppression.

Confidence Score: 3/5

  • The core generation change is correct, but the server guard in api.py incorrectly blocks tool requests for a valid config variant (single_call.enabled=True with no user_messages), causing real 422 errors for users in that configuration.
  • The has_dialog_flows check in api.py includes llm_rails.config.rails.dialog.single_call.enabled in the rejection condition, but the generation code routes single_call.enabled=True && user_messages={} configs through _emit_general_bot_turn, which does surface tool calls. A user with that config receives a spurious 422 error from the server even though the underlying pipeline would handle their tools correctly. This mismatch between the guard and the actual generation behavior is a concrete incorrect outcome introduced by this PR.
  • nemoguardrails/server/api.py — the has_dialog_flows guard needs a closer look to align it with what the generation layer actually does.

Important Files Changed

Filename Overview
nemoguardrails/server/api.py Splits the tool-request guard into separate streaming and dialog-flow checks. The dialog-flow check (bool(user_messages) or single_call.enabled) is overly broad — configs with single_call.enabled=True but no user_messages would actually surface tool calls via _emit_general_bot_turn, but the server incorrectly returns 422 for them.
nemoguardrails/actions/llm/generation.py Removes the passthrough guard around get_and_clear_tool_calls_contextvar() so tool calls surface from any config that reaches _emit_general_bot_turn. The logic is correct: the dialog-flow paths never call this helper, so they naturally drop tool calls.
tests/test_tool_calling_passthrough_integration.py Adds a large TestToolCallingNonPassthroughIntegration class. Most tests are solid, but test_tool_calls_fail_in_dialog_flows_non_passthrough uses an unrecognized YAML key (dialog:) and never populates tool_calls_var, so it does not actually test dialog-flow suppression of tool calls.
tests/integrations/langchain/test_tool_calling_passthrough.py Renamed from test_tool_calling_passthrough_only.py; adds TestToolCallingNonPassthrough tests confirming tool calls surface without passthrough. Patch targets updated to nemoguardrails.actions.llm.generation. Previous threads flagged a bound-mock issue and stale docstrings.
tests/test_generation_equivalence.py Adds well-structured tests for the new behavior: test_non_passthrough_tool_calls verifies tool calls surface from the general non-passthrough path; test_tools_not_supported_when_dialog_flows_are_enabled and test_single_call_tools_not_supported verify they are dropped in dialog-flow configs. These are the strongest tests in the PR.
tests/server/test_api.py Updates server tests to match the new behavior — tools accepted without passthrough for no-dialog-flow configs, rejected for dialog-flow and streaming configs. Tests correctly stub user_messages and single_call.enabled on the mock config.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[API Request with tools] --> B{body.stream?}
    B -- yes --> C[422: non-streaming only]
    B -- no --> D{has_dialog_flows?}
    D -- "bool(user_messages) OR single_call.enabled" --> E[422: dialog flows]
    D -- false --> F[generate_async pipeline]

    F --> G[generate_user_intent]
    G --> H{single_call.enabled?}
    H -- yes --> I[generate_intent_steps_message]
    I --> J{user_messages?}
    J -- yes --> K[Dialog flow path\nBotMessage — tool calls dropped]
    J -- no --> L[_emit_general_bot_turn]
    H -- no --> M{user_messages?}
    M -- yes --> N[_detect_user_intent\nDialog flow path — tool calls dropped]
    M -- no --> L

    L --> O[get_and_clear_tool_calls_contextvar]
    O --> P{tool_calls?}
    P -- yes --> Q[BotToolCalls event]
    P -- no --> R[BotMessage event]

    style E fill:#f99,stroke:#c00
    style C fill:#f99,stroke:#c00
    style K fill:#ffd,stroke:#aa0
    style N fill:#ffd,stroke:#aa0
    style Q fill:#9f9,stroke:#090
    style R fill:#9f9,stroke:#090
Loading
Prompt To Fix All With AI
### Issue 1
nemoguardrails/server/api.py:558
**`single_call.enabled` without `user_messages` incorrectly blocks tools**

The guard rejects tool requests whenever `single_call.enabled` is `True`, but the generation code only drops tool calls when **both** `single_call.enabled` is `True` and `user_messages` is non-empty. When `single_call.enabled=True && user_messages={}`, `generate_intent_steps_message` falls through to `_emit_general_bot_turn` (the else-branch at line 1509 of `generation.py`), which calls `get_and_clear_tool_calls_contextvar()` and emits a `BotToolCalls` event as normal. The `test_single_call_general_no_user_messages` test in `test_generation_equivalence.py` already documents this path. A user with that config will receive a spurious 422 "dialog flows" error even though tools would work.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (2): Last reviewed commit: "docs(tool-calling): passthrough is not r..." | Re-trigger Greptile

Comment thread tests/integrations/langchain/test_tool_calling_passthrough.py Outdated
Comment thread tests/integrations/langchain/test_tool_calling_passthrough.py
Comment on lines +566 to 568
),
)
try:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing blank line between the if tools_requested: block and the try: block causes a minor PEP 8 violation and makes the guard clause less visually distinct from the next logical section.

Suggested change
),
)
try:
),
)
try:
Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/server/api.py
Line: 566-568

Comment:
Missing blank line between the `if tools_requested:` block and the `try:` block causes a minor PEP 8 violation and makes the guard clause less visually distinct from the next logical section.

```suggestion
                ),
            )

    try:
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

…are configured

Signed-off-by: Christina Xu <chrxu@redhat.com>
@christinaexyou
christinaexyou force-pushed the pouyanpi/improve-generate-colang branch from 9c8f260 to f47da75 Compare August 7, 2026 18:01

# Dialog-flow configs (canonical-form user_messages or single_call dialog
# rails) never surface tool calls to the request, so reject explicitly
has_dialog_flows = bool(llm_rails.config.user_messages) or llm_rails.config.rails.dialog.single_call.enabled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 single_call.enabled without user_messages incorrectly blocks tools

The guard rejects tool requests whenever single_call.enabled is True, but the generation code only drops tool calls when both single_call.enabled is True and user_messages is non-empty. When single_call.enabled=True && user_messages={}, generate_intent_steps_message falls through to _emit_general_bot_turn (the else-branch at line 1509 of generation.py), which calls get_and_clear_tool_calls_contextvar() and emits a BotToolCalls event as normal. The test_single_call_general_no_user_messages test in test_generation_equivalence.py already documents this path. A user with that config will receive a spurious 422 "dialog flows" error even though tools would work.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/server/api.py
Line: 558

Comment:
**`single_call.enabled` without `user_messages` incorrectly blocks tools**

The guard rejects tool requests whenever `single_call.enabled` is `True`, but the generation code only drops tool calls when **both** `single_call.enabled` is `True` and `user_messages` is non-empty. When `single_call.enabled=True && user_messages={}`, `generate_intent_steps_message` falls through to `_emit_general_bot_turn` (the else-branch at line 1509 of `generation.py`), which calls `get_and_clear_tool_calls_contextvar()` and emits a `BotToolCalls` event as normal. The `test_single_call_general_no_user_messages` test in `test_generation_equivalence.py` already documents this path. A user with that config will receive a spurious 422 "dialog flows" error even though tools would work.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs: signing size: L status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants