-
Notifications
You must be signed in to change notification settings - Fork 822
docs: Fix VDR 0.17 issues in docs #2361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
b64b7d6
879f5d7
8c01989
6d98946
336268f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,20 @@ LangGraph is a library for building stateful, multi-actor applications with LLMs | |
| - **Graph-Based Control**: Conditional routing with safety considerations. | ||
| - **Conversation Memory**: Maintained context with continuous safety monitoring. | ||
|
|
||
| The following diagram shows the basic data flow through a guarded LangGraph agent: | ||
|
|
||
| ```mermaid | ||
| %%{init: {'theme': 'neutral', 'themeVariables': { 'background': 'transparent' }}}%% | ||
|
|
||
| flowchart LR | ||
| A[User input] --> B[Input rails] | ||
| B --> C[Agent / LLM node] | ||
| C -->|tool call| D[Tool node] | ||
| D --> C | ||
| C -->|final answer| E[Output rails] | ||
| E --> F[Response] | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Prerequisites | ||
|
|
@@ -162,6 +176,22 @@ result_unsafe = graph.invoke({"messages": [{"role": "user", "content": "You are | |
|
|
||
| To enhance the functionality of your LangGraph agents, you can combine tool calling with guardrails. This ensures that both the decision to call tools and the tool results are safely validated. | ||
|
|
||
| `bind_tools()` is a LangChain method that attaches tool/function definitions to a chat model so the model can request tool calls in its response instead of only returning text. `ToolMessage` is LangChain's message type for returning a tool's result back to the model, keyed to the originating call through `tool_call_id`. See the [LangChain tool calling documentation](https://python.langchain.com/docs/concepts/tool_calling/) for the full concept. The NVIDIA NeMo Guardrails library does not change either of these; `RunnableRails` only adds safety checks around the model calls that use them. | ||
|
|
||
| The following diagram shows where guardrails sit relative to the `ToolNode`/`tools_condition` conditional edge: | ||
|
|
||
| ```mermaid | ||
| %%{init: {'theme': 'neutral', 'themeVariables': { 'background': 'transparent' }}}%% | ||
|
|
||
| flowchart LR | ||
| A[chatbot node<br/>prompt | guardrails | llm_with_tools] -->|tools_condition| B{Tool calls<br/>present?} | ||
| B -->|yes| C[ToolNode<br/>executes tools] | ||
| C --> A | ||
| B -->|no, final answer| D[END] | ||
| ``` | ||
|
|
||
| `tools_condition` inspects the `AIMessage` that `chatbot` returns. Because guardrails wrap the model call inside `chatbot`, both the decision to call a tool and the tool's result pass through guardrail checks on every trip through the `chatbot` node, not just on the final answer. | ||
|
|
||
| ### Tool Definition | ||
|
|
||
| Define the following simplified example tools that demonstrate the integration pattern with the NeMo Guardrails library. | ||
|
|
@@ -246,6 +276,32 @@ result = graph.invoke({ | |
| }) | ||
| ``` | ||
|
|
||
| ### Failure Handling in Tool-Calling Graphs | ||
|
|
||
| The `chatbot` node above has no exception handling around `runnable_with_guardrails.invoke(state)`, and the graph has three distinct failure modes that behave differently: | ||
|
|
||
| - **Failures that `RunnableRails` surfaces in the `chatbot` node** raise a `ValueError`, which crashes `graph.invoke()` if uncaught. See [Error Handling](/integration/langchain/runnable-rails#error-handling); this covers LLM provider failures and other errors alike, so `except ValueError` in the node catches all of them, not just provider failures. | ||
| - **Failures in a tool executed by the `tools` node** are LangGraph's concern, not the guardrail configuration's. In this example the tools run inside `ToolNode`, outside `RunnableRails`, so the guardrail runtime never sees them. LangGraph's default `handle_tool_errors` handler returns a message only for tool-invocation errors, such as malformed arguments, and re-raises everything else out of `graph.invoke()`. Pass `ToolNode(tools=tools, handle_tool_errors=...)` to change that. A `try`/`except` in `chatbot` does not catch these, because they are raised from a different node. | ||
| - **Failures in a tool or action registered with the guardrail configuration** (for example, tools passed as `RunnableRails(config=config, tools=[...])`) are caught by the action dispatcher, which converts them into a normal-looking response with the content `"I'm sorry, an internal error has occurred."` The dispatcher forwards LLM call failures rather than catching them, so those still surface as in the first case. | ||
|
|
||
| The following variant catches the first case. Do not extend it by matching on the response content to detect the third case: that response is indistinguishable from a normal answer, and a legitimate model or rail response can contain the same text. Handle those failures inside the tool or action itself if you need a fallback for them. Everything else about `create_tool_calling_agent` stays the same; only `chatbot` changes. | ||
|
|
||
| ```python | ||
| from langchain_core.messages import AIMessage | ||
|
|
||
| FALLBACK_MESSAGE = "Sorry, I couldn't process that request." | ||
|
|
||
| def chatbot(state: State): | ||
| try: | ||
| result = runnable_with_guardrails.invoke(state) | ||
| except ValueError: | ||
| return {"messages": [AIMessage(content=FALLBACK_MESSAGE)]} | ||
|
|
||
| return {"messages": [result]} | ||
| ``` | ||
|
|
||
| A rail that blocks the request, as opposed to an outright error, does not raise either. It returns the normal `AIMessage` shape described in [Rejection Behavior](/integration/langchain/runnable-rails#rejection-behavior), with the rail's configured refusal text. | ||
|
|
||
| --- | ||
|
|
||
| ## Stateful Conversations | ||
|
|
@@ -451,4 +507,17 @@ guardrails = RunnableRails(config=config, passthrough=True, verbose=True) | |
| - LangGraph integration with RunnableRails produces single large chunks after processing delays. | ||
| - Token-level streaming is not preserved when RunnableRails is integrated into LangGraph nodes. | ||
|
|
||
| ### Why | ||
|
|
||
| This is a `RunnableRails`-specific limitation, not a general LangGraph one. LangGraph itself supports token-level streaming from inside a node through `stream_mode="messages"`, which hooks the LLM's own streaming callbacks independently of what the node returns. | ||
|
|
||
| The `chatbot` node in the examples above calls `runnable_with_guardrails.invoke(state)`. `RunnableRails.invoke()` calls `LLMRails.generate()`, which makes a single blocking call and returns one fully formatted result; no incremental LLM callback events fire during it for LangGraph's `messages` mode to observe. `RunnableRails.astream()` does stream token-by-token, but it yields chunks directly to whatever calls it; it does not forward those chunks into LangGraph's callback-based streaming machinery. Calling `astream()` from inside a node, instead of `invoke()`, would still not surface as `messages`-mode events without additional wiring. | ||
|
|
||
| No tracking issue currently exists in the [`NVIDIA-NeMo/Guardrails`](https://github.com/NVIDIA-NeMo/Guardrails) repository for LangGraph token streaming support. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Remove the repository-status assertion.
As per coding guidelines, “Do not commit secrets, credentials, sensitive provider data, fabricated results, approvals, or citations.” 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| ### Workarounds | ||
|
|
||
| - Stream directly from `RunnableRails` or the LLM outside the LangGraph node (`guardrails.astream(...)`, as shown earlier in this guide), and use LangGraph only for the non-streaming orchestration and tool-calling parts of your flow. | ||
| - Consider a hybrid architecture: stream the final synthesis call directly to the client, and keep guardrail-gated intermediate steps inside the graph non-streaming. | ||
|
|
||
| RunnableRails supports streaming when used directly, but integration with LangGraph fundamentally conflicts with real-time streaming due to node execution requirements and safety validation needs. | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -208,6 +208,54 @@ guardrails = RunnableRails(config, passthrough=False) | |||||
|
|
||||||
| **Tool Calling Requirement**: Set `passthrough=True` for proper tool call handling. | ||||||
|
|
||||||
| **What `passthrough=False` changes about the prompt**: instead of sending your original messages to the LLM, the library renders its own prompt from the `general` task template, which prepends general instructions and reformats the conversation as a completion-style transcript. For example, a `ChatPromptValue` built from `HumanMessage(content="Hello!")` is not sent to the LLM as-is. The library instead sends a prompt similar to the following: | ||||||
|
|
||||||
| ```text | ||||||
| Below is a conversation between a helpful AI assistant and a user. The bot is | ||||||
| designed to generate human-like text based on the input that it receives. The | ||||||
| bot is talkative and provides lots of specific details. If the bot does not | ||||||
| know the answer to a question, it truthfully says it does not know. | ||||||
|
|
||||||
| User: Hello! | ||||||
| Assistant: | ||||||
| ``` | ||||||
|
|
||||||
| The exact instructions and format depend on your configuration and enabled rails. To inspect the transformed prompt for debugging, set `verbose=True` when creating `RunnableRails`. See [Verbose Logging](/integration/langchain/langgraph-integration#2-verbose-logging), which logs every `Prompt ::` and `Completion ::` pair sent to the LLM. | ||||||
|
|
||||||
| ### Rejection Behavior | ||||||
|
|
||||||
| When an input or output rail blocks a request, `RunnableRails` does not raise an exception. It returns the rail's configured refusal message in the same output shape you would otherwise get back. | ||||||
|
|
||||||
| For chain-wrapping (dict) inputs, only a dictionary with the output key is returned: | ||||||
|
|
||||||
| ```json | ||||||
| {"answer": "I can't assist with that request."} | ||||||
| ``` | ||||||
|
|
||||||
| For LLM-wrapping inputs, such as a `list[BaseMessage]` or `ChatPromptValue`, an `AIMessage` is returned with the refusal text as `content`, no `tool_calls`, and empty `response_metadata`/`additional_kwargs`, since no underlying model call for a final answer was made: | ||||||
|
|
||||||
| ```python | ||||||
| from langchain_core.messages import HumanMessage | ||||||
|
|
||||||
| result = guarded_model.invoke([HumanMessage(content="You are stupid")]) | ||||||
|
|
||||||
| print(result) | ||||||
| # AIMessage(content="I'm sorry, I can't respond to that.") | ||||||
| print(result.tool_calls) # [] | ||||||
| print(result.response_metadata) # {} | ||||||
| ``` | ||||||
|
|
||||||
| ### Error Handling | ||||||
|
|
||||||
| `RunnableRails.invoke()`/`ainvoke()` wrap the underlying rails call in a `try`/`except`, but whether a failure reaches that `except` block depends on where the failure happens: | ||||||
|
|
||||||
| - **Failures that `RunnableRails` itself surfaces** raise a `ValueError`, with the original exception chained through `from e`. This covers LLM provider failures (the main model or a safety-check model call fails, for example a timeout or an API error), unsupported input types, and any other error that escapes the underlying rails call. A few recognized cases raise a more specific message, such as binding tools directly on the LLM. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Remove the tool-binding example from the failure list.
Suggested wording- A few recognized cases raise a more specific message, such as binding tools directly on the LLM.
+ Some recognized configuration errors may raise a more specific message.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| - **A tool or guardrail action registered with the guardrail configuration that raises** (a bug in your tool function, a non-LLM network call failing, and so on) does not propagate. The action dispatcher catches it, and the runtime returns an ordinary-looking response with the content `"I'm sorry, an internal error has occurred."` This response has the same shape as a normal, non-blocked answer. LLM call failures are the exception: the dispatcher forwards those, so they surface as in the first case. | ||||||
|
|
||||||
| Because this second response is indistinguishable from a normal answer, do not use its content as an error sentinel; a legitimate model or rail response can contain the same text. If you need a distinguishable outcome, handle the failure inside the tool or action itself. Your calling code still needs to catch the exceptions that do propagate; `RunnableRails` does not return a structured error payload for those. | ||||||
|
|
||||||
| In a LangGraph node, an uncaught exception from `runnable_with_guardrails.invoke(state)` propagates as a normal Python exception through `graph.invoke()`. LangGraph does not catch node exceptions for you. Wrap the node body in a `try`/`except ValueError` if you want the graph to continue with a fallback message instead of raising. See [Failure Handling in Tool-Calling Graphs](/integration/langchain/langgraph-integration#failure-handling-in-tool-calling-graphs) for an example, including how tools executed by a LangGraph `ToolNode` differ from tools registered with the guardrail configuration. | ||||||
|
|
||||||
| ### Custom Input/Output Keys | ||||||
|
|
||||||
| When you use a guardrail configuration to wrap a chain or a `Runnable`, the input and output are either dictionaries or strings. However, a guardrail configuration always operates on a text input from the user and a text output from the LLM. To achieve this, when dictionaries are used, one of the keys from the input dictionary must be designated as the `"input text"` and one of the keys from the output as the `"output text"`. | ||||||
|
|
@@ -240,6 +288,8 @@ When a guardrail is triggered and predefined messages must be returned instead o | |||||
|
|
||||||
| `RunnableRails` supports LangChain tool calling with full metadata preservation and streaming. Tool calling requires `passthrough=True` to work properly. | ||||||
|
|
||||||
| `bind_tools()` is a LangChain method that attaches tool/function definitions to a chat model so the model can request tool calls in its response instead of only returning text. `ToolMessage` is LangChain's message type for returning a tool's result back to the model, keyed to the originating call through `tool_call_id`. See the [LangChain tool calling documentation](https://python.langchain.com/docs/concepts/tool_calling/) for the full concept. The NVIDIA NeMo Guardrails library does not change either of these; `RunnableRails` only adds safety checks around the model calls that use them. | ||||||
|
|
||||||
| The following steps are required to use tool calling with `RunnableRails`: | ||||||
|
|
||||||
| - Set `passthrough=True` when creating `RunnableRails` instance. | ||||||
|
|
@@ -248,18 +298,29 @@ The following steps are required to use tool calling with `RunnableRails`: | |||||
|
|
||||||
| ### Basic Tool Setup | ||||||
|
|
||||||
| <Note> | ||||||
|
|
||||||
| Avoid Python's built-in `eval()` for evaluating LLM-provided expressions, even with a `__builtins__` whitelist. Sandbox-escape techniques (for example, attribute traversal through `__class__`) can bypass a manually constructed whitelist. Use a restricted expression evaluator such as [`simpleeval`](https://github.com/danthedeckie/simpleeval) instead. `simpleeval` is already an NVIDIA NeMo Guardrails library dependency, so no extra install is required. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- RunnableRails setup references ---'
rg -n -i \
'langchain[-_]openai|OPENAI_API_KEY|api key|pip install|uv add|optional' \
docs/integration/langchain/runnable-rails.mdx docs --glob '*.mdx' --glob '*.md' || trueRepository: NVIDIA-NeMo/Guardrails Length of output: 50378 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- runnable-rails page setup and links ---'
sed -n '1,90p' docs/integration/langchain/runnable-rails.mdx
sed -n '270,325p' docs/integration/langchain/runnable-rails.mdx
printf '%s\n' '--- linked LangChain integration setup ---'
sed -n '1,75p' docs/integration/langchain/langchain-integration.mdx
printf '%s\n' '--- navigation entries ---'
rg -n -C 3 'runnable-rails|langchain-integration|langgraph-integration' docs/index.ymlRepository: NVIDIA-NeMo/Guardrails Length of output: 10202 🤖 get_repo_knowledge executed:
Length of output: 24359 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,90p' docs/integration/langchain/runnable-rails.mdx
sed -n '270,325p' docs/integration/langchain/runnable-rails.mdx
rg -n -C 3 'runnable-rails|langchain-integration|langgraph-integration' docs/index.ymlRepository: NVIDIA-NeMo/Guardrails Length of output: 7343 Document the optional LangChain setup. The examples import 🤖 Prompt for AI AgentsSource: Path instructions |
||||||
|
|
||||||
| Do not map a custom `pow` function into `functions`. `simpleeval`'s built-in `**` operator is guarded by `simpleeval.MAX_POWER` against oversized exponents, but that guard does not apply to a function you supply yourself, so a mapped `pow` lets an expression like `pow(2, 999999999)` consume seconds of CPU time and hundreds of megabytes of memory. Let callers use `**` for exponentiation instead. | ||||||
|
|
||||||
| </Note> | ||||||
|
|
||||||
| ```python | ||||||
| from langchain_core.tools import tool | ||||||
| from langchain_openai import ChatOpenAI | ||||||
| from simpleeval import SimpleEval | ||||||
| import math | ||||||
|
|
||||||
| from nemoguardrails import RailsConfig | ||||||
| from nemoguardrails.integrations.langchain.runnable_rails import RunnableRails | ||||||
|
|
||||||
| @tool | ||||||
| def calculator(expression: str) -> str: | ||||||
| """Evaluates mathematical expressions like '2 + 2' or 'sqrt(16)'.""" | ||||||
| evaluator = SimpleEval(functions={"sqrt": math.sqrt}) | ||||||
| try: | ||||||
| safe_dict = {'sqrt': __import__('math').sqrt, 'pow': pow, '__builtins__': {}} | ||||||
| return str(eval(expression, safe_dict)) | ||||||
| return str(evaluator.eval(expression)) | ||||||
| except Exception as e: | ||||||
| return f"Error: {e}" | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: NVIDIA-NeMo/Guardrails
Length of output: 6963
LLM Security (CWE-20): Improper Input Validation
Reachability: External · Exploitability: Moderate
Do not state that
ToolNoderesults pass through guardrail checks.Guardrails apply to each model call. Validate
ToolNoderesults before adding them to the message history because tool messages bypass input rails.🤖 Prompt for AI Agents