-
Notifications
You must be signed in to change notification settings - Fork 770
feat(iorails)!: Support non-streaming GenerationResponse #2178
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
49ae76f
054dca7
b72afc0
07c4b29
604deab
95c0a74
3001e83
a04d242
559ace5
7912d6c
cdee6c3
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 |
|---|---|---|
|
|
@@ -38,7 +38,7 @@ | |
| from nemoguardrails.logging.explain import ExplainInfo | ||
| from nemoguardrails.rails.llm.config import RailsConfig | ||
| from nemoguardrails.rails.llm.llmrails import LLMRails | ||
| from nemoguardrails.rails.llm.options import GenerationResponse, RailsResult, RailType | ||
| from nemoguardrails.rails.llm.options import GenerationOptions, GenerationResponse, RailsResult, RailType | ||
| from nemoguardrails.types import LLMModel | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
@@ -205,43 +205,69 @@ async def _ensure_started(self) -> None: | |
| await self.startup() | ||
|
|
||
| def generate( | ||
| self, prompt: str | None = None, messages: LLMMessages | None = None, **kwargs | ||
| self, | ||
| prompt: str | None = None, | ||
| messages: LLMMessages | None = None, | ||
| options: Optional[Union[dict, GenerationOptions]] = None, | ||
| **kwargs, | ||
| ) -> Union[str, dict, GenerationResponse, Tuple[dict, dict]]: | ||
| """Generate an LLM response synchronously with guardrails applied. | ||
| Supported in both IORails and LLMRails | ||
| """ | ||
|
|
||
| generate_messages = self._convert_to_messages(prompt, messages) | ||
| return self.rails_engine.generate(messages=generate_messages, **kwargs) | ||
| return self.rails_engine.generate(messages=generate_messages, options=options, **kwargs) | ||
|
|
||
| @overload | ||
| async def generate_async(self, prompt: str | None = None, messages: LLMMessages | None = None, **kwargs) -> str: ... | ||
| async def generate_async( | ||
| self, | ||
| prompt: str | None = None, | ||
| messages: LLMMessages | None = None, | ||
| options: Optional[Union[dict, GenerationOptions]] = None, | ||
| **kwargs, | ||
| ) -> str: ... | ||
|
|
||
| @overload | ||
| async def generate_async( | ||
| self, prompt: str | None = None, messages: LLMMessages | None = None, **kwargs | ||
| self, | ||
| prompt: str | None = None, | ||
| messages: LLMMessages | None = None, | ||
| options: Optional[Union[dict, GenerationOptions]] = None, | ||
| **kwargs, | ||
| ) -> dict: ... | ||
|
|
||
| @overload | ||
| async def generate_async( | ||
| self, prompt: str | None = None, messages: LLMMessages | None = None, **kwargs | ||
| self, | ||
| prompt: str | None = None, | ||
| messages: LLMMessages | None = None, | ||
| options: Optional[Union[dict, GenerationOptions]] = None, | ||
| **kwargs, | ||
| ) -> GenerationResponse: ... | ||
|
|
||
| @overload | ||
| async def generate_async( | ||
| self, prompt: str | None = None, messages: LLMMessages | None = None, **kwargs | ||
| self, | ||
| prompt: str | None = None, | ||
| messages: LLMMessages | None = None, | ||
| options: Optional[Union[dict, GenerationOptions]] = None, | ||
| **kwargs, | ||
| ) -> tuple[dict, dict]: ... | ||
|
Comment on lines
221
to
255
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 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the relevant method and nearby definitions.
file="nemoguardrails/guardrails/guardrails.py"
wc -l "$file"
sed -n '180,310p' "$file"
# Find the concrete implementation and any related overloads in the repo.
rg -n "`@overload`|def generate_async|def generate\(" nemoguardrails/guardrails/guardrails.py nemoguardrails -g '*.py'Repository: NVIDIA-NeMo/Guardrails Length of output: 7333 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the underlying LLMRails overloads and any public type declarations.
sed -n '1340,1415p' nemoguardrails/rails/llm/llmrails.py
sed -n '1415,1515p' nemoguardrails/rails/llm/llmrails.py
sed -n '220,290p' nemoguardrails/types.py
sed -n '1,90p' nemoguardrails/base_guardrails.pyRepository: NVIDIA-NeMo/Guardrails Length of output: 11942 Make the async overloads distinguish All four 🤖 Prompt for AI Agents |
||
|
|
||
| async def generate_async( | ||
| self, prompt: str | None = None, messages: LLMMessages | None = None, **kwargs | ||
| self, | ||
| prompt: str | None = None, | ||
| messages: LLMMessages | None = None, | ||
| options: Optional[Union[dict, GenerationOptions]] = None, | ||
| **kwargs, | ||
| ) -> str | dict | GenerationResponse | tuple[dict, dict]: | ||
| """Generate an LLM response asynchronously with guardrails applied. | ||
| Supported by both LLMRails and IORails | ||
| """ | ||
| await self._ensure_started() | ||
|
|
||
| generate_messages = self._convert_to_messages(prompt, messages) | ||
| return await self.rails_engine.generate_async(messages=generate_messages, **kwargs) | ||
| return await self.rails_engine.generate_async(messages=generate_messages, options=options, **kwargs) | ||
|
|
||
| def stream_async( | ||
| self, prompt: str | None = None, messages: LLMMessages | None = None, **kwargs | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,9 +16,11 @@ | |
|
|
||
| import secrets | ||
| from contextvars import ContextVar, Token | ||
| from dataclasses import dataclass | ||
| from dataclasses import dataclass, field | ||
| from enum import Enum | ||
| from typing import Any, TypeAlias | ||
| from typing import Any, Optional, TypeAlias | ||
|
|
||
| from nemoguardrails.types import UsageInfo | ||
|
|
||
| # LLMMessage can contain role/content, plus optional tool_calls / tool_call_id / name; content may be None | ||
| LLMMessage: TypeAlias = dict[str, Any] | ||
|
|
@@ -32,13 +34,56 @@ class RailDirection(Enum): | |
| OUTPUT = "Output" | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
| class RailCallRecord: | ||
| """One rail's execution record, carried on RailResult for GenerationLog synthesis. | ||
| Captures what a single rail did — its verdict and the (at most one) model call it | ||
| made — as engine-neutral data. IORails maps a ``RailCallRecord`` to an | ||
| ``ActivatedRail`` (with a single synthetic ``ExecutedAction`` and ``LLMCallInfo``); | ||
| the raw ``usage``/timing is kept here so this module stays free of the pydantic | ||
| ``GenerationLog`` types. Tool rails that make no model call leave ``usage`` None. | ||
| """ | ||
|
|
||
| flow: str | ||
| rail_type: str | ||
| is_safe: bool | ||
| made_call: bool = False | ||
| action_name: Optional[str] = None | ||
| return_value: Any = None | ||
| task: Optional[str] = None | ||
| request_id: Optional[str] = None | ||
| usage: Optional[UsageInfo] = None | ||
| llm_model_name: Optional[str] = None | ||
| llm_provider_name: Optional[str] = None | ||
| prompt: Optional[str] = None | ||
| completion: Optional[str] = None | ||
| started_at: Optional[float] = None | ||
| finished_at: Optional[float] = None | ||
| duration: Optional[float] = None | ||
|
|
||
|
|
||
| @dataclass(frozen=True, slots=True) | ||
| class RailResult: | ||
| """Result of a rail safety check.""" | ||
| """Result of a rail safety check. | ||
| ``records`` carries the per-rail execution records for every rail that ran in this | ||
| check (not just the blocking one), so IORails can synthesize a ``GenerationLog``. | ||
| It is empty unless log collection is active. ``return_value`` is the rail's | ||
| structured verdict (e.g. ``{"allowed": ..., "policy_violations": [...]}``) when the | ||
| action supplies one, used as the log's ``ExecutedAction.return_value``. | ||
| ``records`` and ``return_value`` are log-capture metadata, not part of the safety | ||
| verdict, so they are excluded from equality and hashing (``compare=False``) — two | ||
| results with the same ``is_safe``/``reason``/``triggered_rail`` compare equal | ||
| regardless of captured log data. | ||
| """ | ||
|
|
||
| is_safe: bool | ||
| reason: str | None = None | ||
| triggered_rail: str | None = None | ||
| records: tuple[RailCallRecord, ...] = field(default=(), compare=False) | ||
| return_value: Any = field(default=None, compare=False) | ||
|
|
||
|
|
||
| # Default max character length for truncate(). Used to keep DEBUG log lines short. | ||
|
|
@@ -84,3 +129,13 @@ def truncate(text: object, max_len: int | None = None) -> str: | |
| if len(s) <= limit: | ||
| return s | ||
| return s[:limit] + "..." | ||
|
|
||
|
|
||
| def serialize_prompt(messages: list[dict]) -> str: | ||
| """Render a chat message list to a role-labeled string for GenerationLog's ``prompt``. | ||
| Content parity with LLMRails' logged prompt, not byte-for-byte format parity: each | ||
| message becomes ``"<role>: <content>"`` and messages are blank-line separated. A | ||
| message with no content (e.g. a reasoning-only or tool-call turn) renders as empty. | ||
| """ | ||
| return "\n\n".join(f"{m.get('role', '')}: {m.get('content') or ''}" for m in messages) | ||
|
Comment on lines
+134
to
+141
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Preserve non-text fields in captured prompts. Line 141 discards fields such as As per coding guidelines, preserve non-text model metadata and do not drop reasoning-only content. 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
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.
return_valuehere is a rawbool(thejailbreakfield), whilecontent_safety_actionreturns{"allowed": bool, "policy_violations": list}andtopic_safety_actionreturns{"on_topic": bool}. Consumers iteratingGenerationLog.activated_rails[].executed_actions[].return_valuewill see inconsistent types across rail kinds; a dict keeps all three uniform.Prompt To Fix With AI
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!