Skip to content

Conversation

@Pouyanpi
Copy link
Collaborator

@Pouyanpi Pouyanpi commented Jan 9, 2026

Description

review #1384 to limit functional changes and fix pyright errors.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Overview

Greptile Summary

This PR attempts to clean up type annotations and simplify code in the guardrails integration, but introduces several logic bugs that will cause runtime errors and breaking behavior changes.

Major Issues Found

  • Removed null safety check in passthrough_fn that will cause runtime errors when passthrough_input is missing from context
  • Breaking change in batch() method that lost per-input configuration support - all batched inputs now share the first config instead of individual configs
  • Added return_exceptions parameters to batch methods but completely unused, misleading callers about exception handling behavior
  • Removed safety checks in _extract_text_from_input() causing KeyError for missing dictionary keys and wrong string conversion for None values
  • Changed getattr() pattern for duck-typed objects that alters behavior when attributes exist but are falsy

Recommendations

This PR should not be merged as-is. The type annotation improvements in generation.py are good, but the changes in runnable_rails.py introduce too many functional regressions. Suggest reverting the functional changes and keeping only the type annotation updates.

Confidence Score: 1/5

  • This PR is not safe to merge - contains multiple logic bugs that will cause runtime errors and breaking behavior changes
  • Score reflects 8 critical logic issues found: removed null checks will cause runtime errors, batch() method breaks per-input configs (breaking change for existing users), unused return_exceptions parameters mislead callers, and removed safety checks cause KeyErrors. Only the generation.py changes are safe.
  • Pay close attention to nemoguardrails/integrations/langchain/runnable_rails.py - it has multiple critical bugs that need fixing before merge

Important Files Changed

File Analysis

Filename Score Overview
nemoguardrails/actions/llm/generation.py 5/5 Updated copyright year to 2026 and added Tuple to type imports. Updated passthrough_fn type annotation to accept tuple returns. These are safe, well-aligned type annotation improvements.
nemoguardrails/integrations/langchain/runnable_rails.py 2/5 Updated copyright year, reorganized imports, removed type annotations, simplified multiple methods. Contains several issues: removed null check in passthrough_fn, broke per-input config handling in batch(), added unused return_exceptions parameter, removed safety checks in _extract_text_from_input.

Sequence Diagram

sequenceDiagram
    participant User
    participant RunnableRails
    participant LLMGenerationActions
    participant Runnable
    
    User->>RunnableRails: batch(inputs, config_list)
    Note over RunnableRails: BUG: Uses config[0] for all inputs<br/>instead of individual configs
    
    RunnableRails->>RunnableRails: invoke(input, config[0])
    RunnableRails->>RunnableRails: _transform_input_to_rails_format()
    
    alt Passthrough mode with runnable
        RunnableRails->>RunnableRails: _create_passthrough_messages()
        RunnableRails->>RunnableRails: _extract_text_from_input()
        Note over RunnableRails: BUG: Can raise KeyError<br/>on missing dict key
        RunnableRails->>LLMGenerationActions: passthrough_fn(context, events)
        Note over LLMGenerationActions: BUG: _input can be None<br/>no null check
        LLMGenerationActions->>Runnable: ainvoke(_input, config)
        Runnable-->>LLMGenerationActions: output
        LLMGenerationActions-->>RunnableRails: (text, output)
    end
    
    RunnableRails->>RunnableRails: rails.generate()
    RunnableRails->>RunnableRails: _format_output()
    RunnableRails-->>User: results
Loading

async def passthrough_fn(context: dict, events: List[dict]) -> tuple[str, Any]:
async def passthrough_fn(context: dict, events: List[dict]):
# First, we fetch the input from the context
_input = context.get("passthrough_input")
Copy link
Contributor

Choose a reason for hiding this comment

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

Removed null check - context.get("passthrough_input") can return None, which will be passed directly to runnable.ainvoke(). This could cause runtime errors if the runnable doesn't handle None input gracefully.

Previous code checked if _input is None: raise ValueError(...) which provided clear error messaging.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/integrations/langchain/runnable_rails.py
Line: 121:121

Comment:
Removed null check - `context.get("passthrough_input")` can return `None`, which will be passed directly to `runnable.ainvoke()`. This could cause runtime errors if the runnable doesn't handle `None` input gracefully.

Previous code checked `if _input is None: raise ValueError(...)` which provided clear error messaging.

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

Comment on lines +852 to +854
single_config = config[0] if isinstance(config, list) else config
return [self.invoke(input, single_config, **kwargs) for input in inputs]
Copy link
Contributor

Choose a reason for hiding this comment

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

Breaking change - lost per-input config support. Previous code handled config as a list by zipping with inputs: [self.invoke(inp, cfg, **kwargs) for inp, cfg in zip(inputs, config)]

Now always uses config[0] when config is a list, meaning all inputs use the same config. This breaks use cases where each input needs different configuration (e.g., different callbacks, metadata, or max_concurrency per input).

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/integrations/langchain/runnable_rails.py
Line: 852:853

Comment:
Breaking change - lost per-input config support. Previous code handled `config` as a list by zipping with inputs: `[self.invoke(inp, cfg, **kwargs) for inp, cfg in zip(inputs, config)]`

Now always uses `config[0]` when config is a list, meaning all inputs use the same config. This breaks use cases where each input needs different configuration (e.g., different callbacks, metadata, or max_concurrency per input).

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

inputs: List[Input],
config: Optional[Union[RunnableConfig, List[RunnableConfig]]] = None,
*,
return_exceptions: bool = False,
Copy link
Contributor

Choose a reason for hiding this comment

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

Added return_exceptions parameter but it's completely unused in the implementation. This is misleading - callers will expect exception handling behavior that doesn't exist.

Either implement the parameter (catch exceptions and return them in results when True) or remove it from the signature.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/integrations/langchain/runnable_rails.py
Line: 848:848

Comment:
Added `return_exceptions` parameter but it's completely unused in the implementation. This is misleading - callers will expect exception handling behavior that doesn't exist.

Either implement the parameter (catch exceptions and return them in results when True) or remove it from the signature.

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

inputs: List[Input],
config: Optional[Union[RunnableConfig, List[RunnableConfig]]] = None,
*,
return_exceptions: bool = False,
Copy link
Contributor

Choose a reason for hiding this comment

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

Added return_exceptions parameter but it's not used. The parameter is not passed to gather_with_concurrency, so exceptions will always propagate instead of being returned in the results when return_exceptions=True.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/integrations/langchain/runnable_rails.py
Line: 860:860

Comment:
Added `return_exceptions` parameter but it's not used. The parameter is not passed to `gather_with_concurrency`, so exceptions will always propagate instead of being returned in the results when `return_exceptions=True`.

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

Comment on lines +202 to +205
return str(_input.content)
elif isinstance(_input, dict) and self.passthrough_user_input_key in _input:
value = _input.get(self.passthrough_user_input_key)
return str(value) if value is not None else ""
return str(_input[self.passthrough_user_input_key])
Copy link
Contributor

Choose a reason for hiding this comment

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

Removed safety check - if _input.content is None or empty, str(None) returns "None" (the string). Previous code returned empty string for None/falsy content.

This changes behavior: str(None) != "". Could break logic expecting empty strings for missing content.

Suggested change
return str(_input.content)
elif isinstance(_input, dict) and self.passthrough_user_input_key in _input:
value = _input.get(self.passthrough_user_input_key)
return str(value) if value is not None else ""
return str(_input[self.passthrough_user_input_key])
elif is_base_message(_input):
return str(_input.content) if _input.content else ""
Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/integrations/langchain/runnable_rails.py
Line: 202:204

Comment:
Removed safety check - if `_input.content` is `None` or empty, `str(None)` returns `"None"` (the string). Previous code returned empty string for `None`/falsy content.

This changes behavior: `str(None)` != `""`. Could break logic expecting empty strings for missing content.

```suggestion
        elif is_base_message(_input):
            return str(_input.content) if _input.content else ""
```

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

elif isinstance(_input, dict) and self.passthrough_user_input_key in _input:
value = _input.get(self.passthrough_user_input_key)
return str(value) if value is not None else ""
return str(_input[self.passthrough_user_input_key])
Copy link
Contributor

Choose a reason for hiding this comment

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

Direct dictionary access will raise KeyError if passthrough_user_input_key is not in _input. Previous code used .get() with fallback to handle missing keys safely.

Suggested change
return str(_input[self.passthrough_user_input_key])
return str(_input.get(self.passthrough_user_input_key, ""))
Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/integrations/langchain/runnable_rails.py
Line: 204:204

Comment:
Direct dictionary access will raise `KeyError` if `passthrough_user_input_key` is not in `_input`. Previous code used `.get()` with fallback to handle missing keys safely.

```suggestion
            return str(_input.get(self.passthrough_user_input_key, ""))
```

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

else:
# For duck-typed objects (including mocks in tests)
context = getattr(res, "output_data", None) or {}
context = getattr(res, "output_data", {}) or {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Changed from getattr(res, "output_data", None) or {} to getattr(res, "output_data", {}) or {}.

The new code will return {} when output_data exists but is falsy (e.g., empty dict, None, 0, False). Previous code only used default {} when attribute didn't exist, preserving falsy but existing values. This changes behavior for duck-typed objects.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/integrations/langchain/runnable_rails.py
Line: 648:648

Comment:
Changed from `getattr(res, "output_data", None) or {}` to `getattr(res, "output_data", {}) or {}`.

The new code will return `{}` when `output_data` exists but is falsy (e.g., empty dict, None, 0, False). Previous code only used default `{}` when attribute didn't exist, preserving falsy but existing values. This changes behavior for duck-typed objects.

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

else:
# For duck-typed objects (including mocks in tests)
context = getattr(res, "output_data", None) or {}
context = getattr(res, "output_data", {}) or {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Same issue as line 648 - behavior change for duck-typed objects. When output_data attribute exists but is falsy, this will return {} instead of preserving the falsy value.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/integrations/langchain/runnable_rails.py
Line: 720:720

Comment:
Same issue as line 648 - behavior change for duck-typed objects. When `output_data` attribute exists but is falsy, this will return `{}` instead of preserving the falsy value.

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

@codecov
Copy link

codecov bot commented Jan 9, 2026

Codecov Report

❌ Patch coverage is 82.85714% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...uardrails/integrations/langchain/runnable_rails.py 81.81% 6 Missing ⚠️

📢 Thoughts on this report? Let us know!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants