Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/integrations/nemo_guardrails/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
models:
- type: main
engine: openai
model: gpt-3.5-turbo-instruct
79 changes: 79 additions & 0 deletions docs/integrations/nemo_guardrails/guardrails_rails.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
This guide will teach you how to add guardrails configurations built with NeMo Guardrails to your Guardrails AI application.

# Overview

The Guardrails AI library provides a Rails integration that allows you to use a Rails application as an LLM callable. This will result in a Rails application that generates completions that are validated using a GuardrailsAI guard configuration.

We start by defining a Guardrails AI Guard and a Rails configuration. We'll also install the [ToxicLanguage validator](https://hub.guardrailsai.com/validator/guardrails/toxic_language) from the [Guardrails AI Hub](https://hub.guardrailsai.com/).

```python
from nemoguardrails import LLMRails, RailsConfig
from guardrails import Guard, install

install("hub://guardrails/toxic_language")
from guardrails.hub import ToxicLanguage

# Load a guardrails configuration from the specified path.
config = RailsConfig.from_path("PATH/TO/CONFIG")
rails = LLMRails(config)
```

Then, we have the guard validate the completions generated by the Rails application.

```python
from guardrails.integrations.nemoguardrails.nemoguardrails_guard import (
NemoguardrailsGuard
)
railsguard = NemoguardrailsGuards(rails).use(ToxicLanguage)

result = railsguard(
messages: [{
"role":"user",
"content":"Hello! What can you do for me?"
}]
)
```

The `NemoguardrailsGuard` class is a wrapper around the Guard class. Just like a Guard, it can [called](https://www.guardrailsai.com/docs/api_reference_markdown/guards#__call__) with similar parameters to the OpenAI completions API. It also returns a `ValidationOutcome` object (or iterable, in streaming cases). That object can be destructured to get the raw output, the validated output, and other metadata.

Here, `raw_llm_output` is the output returned by the NeMo Guardrails Rails.

```
result.raw_llm_output
result.validated_output
result.validation_passed
```

## Expected NeMo Guardrails Rails output

The NeMo Guardrails Rails may return any serializable type expressable in python using native types or Pydantic. The output must conform to the datatypes expected by the specified Guard. If the output is structured, make sure to initialize the Guardrails AI Guard using pydantic, [following this guide](https://www.guardrailsai.com/docs/how_to_guides/generate_structured_data).

# Integration with the NeMo Guardrails server

To wrap a call to the NeMo Guardrails server, we can leverage the OpenAI-style API endpoint available. We can talk to this endpoint directly through the Guard, setting the correct endpoint and config_id.


First, start the NeMo Guardrails server:

```bash
nemoguardrails server [--config PATH/TO/CONFIGS] [--port PORT]
```

Then, talk to it using the Guard:

```python
from guardrails import Guard
guard = Guard.use(
ToxicLanguage()
)

# invoke the guard using the endpoint and config_id
guard(
endpoint="http://localhost:8000/v1/chat/completions",
config_id="CONFIG_ID",
messages: [{
"role":"user",
"content":"Hello! What can you do for me?"
}]
)
```
52 changes: 52 additions & 0 deletions docs/integrations/nemo_guardrails/rails_guardrails.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
:::
note: This will exist in the NeMo Guardrails docs
:::


# Introduction

Integrating Guardrails AI with NeMo Guardrails combines the strengths of both frameworks:

Guardrails AI's extensive hub of validators can enhance NeMo Guardrails' input and output checking capabilities.
NeMo Guardrails' flexible configuration system can provide a powerful context for applying Guardrails AI validators.
Users of both frameworks can benefit from a seamless integration, reducing development time and improving overall safety measures.
This integration allows developers to leverage the best features of both frameworks, creating more robust and secure LLM applications.

# Overview
This document provides a guide to using a Guardrails AI Guard as an action within a NeMo Guardrails Rails application. This can be done either by defining an entire Guard and registering it, or by registering a validator directly.

## Registering a Guard as an action

First, we install our validators and define our Guard

```python
from guardrails import Guard, install
install("hub://guardrails/toxic_language")
from guardrails.hub import ToxicLanguage

guard = Guard().use(
ToxicLanguage()
)
```

Next, we register our `guard` using the nemoguardrails registration API

```python
from nemoguardrails import RailsConfig, LLMRails

config = RailsConfig.from_path("path/to/config")
rails = LLMRails(config)

rails.register_action(guard, "custom_guard_action")
```

Now, the `custom_guard_action` can be used as an action within the Rails specification. This action can be used on input or output, and may be used in any number of flows.

```yaml
define flow
...
$result = execute custom_guard_action
...
```


Empty file.
217 changes: 217 additions & 0 deletions guardrails/integrations/nemoguardrails/nemoguardrails_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
from typing import Callable, Dict, Iterable, List, Optional, Union, cast
import warnings
from typing_extensions import deprecated

from guardrails.classes.execution.guard_execution_options import GuardExecutionOptions
from guardrails.classes.output_type import OT, OutputTypes
from guardrails.classes.validation_outcome import ValidationOutcome

from guardrails import Guard
from nemoguardrails import LLMRails

from guardrails.formatters import get_formatter
from guardrails.formatters.base_formatter import BaseFormatter
from guardrails.schema.pydantic_schema import pydantic_model_to_schema
from guardrails.types.pydantic import ModelOrListOfModels

from guardrails.stores.context import (
Tracer
)


class NemoguardrailsGuard(Guard):
def __init__(
self,
nemorails: LLMRails,
*args,
**kwargs,
):
super().__init__(*args, **kwargs)
self._nemorails = nemorails

def __call__(
self, llm_api: Optional[Callable] = None, generate_kwargs: Optional[Dict] = None, *args, **kwargs
) -> Union[ValidationOutcome[OT], Iterable[ValidationOutcome[OT]]]:
# peel llm_api off of kwargs
llm_api = kwargs.pop("llm_api", None)

# if llm_api is defined, throw an error
if llm_api is not None:
raise ValueError(
"""llm_api should not be passed to a NemoguardrailsGuard object.
The Nemoguardrails LLMRails object passed in will be used as the LLM."""
)

# peel off messages from kwargs
messages = kwargs.get("messages", None)

# if messages is not defined, throw an error
if messages is None:
raise ValueError(
"""messages should be passed to a NemoguardrailsGuard object.
The messages to be passed to the LLM should be passed in as a list of
dictionaries, where each dictionary has a 'role' key and a 'content' key."""
)

def _custom_nemo_callable(*args, **kwargs):
return self._custom_nemo_callable(*args, generate_kwargs=generate_kwargs, **kwargs)

return super().__call__(llm_api=_custom_nemo_callable, *args, **kwargs)

@classmethod
def from_pydantic(
cls,
nemorails: LLMRails,
output_class: ModelOrListOfModels,
*,
prompt: Optional[str] = None,
instructions: Optional[str] = None,
num_reasks: Optional[int] = None,
reask_prompt: Optional[str] = None,
reask_instructions: Optional[str] = None,
reask_messages: Optional[List[Dict]] = None,
messages: Optional[List[Dict]] = None,
tracer: Optional[Tracer] = None,
name: Optional[str] = None,
description: Optional[str] = None,
output_formatter: Optional[Union[str, BaseFormatter]] = None,
):
"""Create a Guard instance using a Pydantic model to specify the output
schema.

Args:
output_class: (Union[Type[BaseModel], List[Type[BaseModel]]]): The pydantic model that describes
the desired structure of the output.
prompt (str, optional): The prompt used to generate the string. Defaults to None.
instructions (str, optional): Instructions for chat models. Defaults to None.
reask_prompt (str, optional): An alternative prompt to use during reasks. Defaults to None.
reask_instructions (str, optional): Alternative instructions to use during reasks. Defaults to None.
reask_messages (List[Dict], optional): A list of messages to use during reasks. Defaults to None.
num_reasks (int, optional): The max times to re-ask the LLM if validation fails. Deprecated
tracer (Tracer, optional): An OpenTelemetry tracer to use for metrics and traces. Defaults to None.
name (str, optional): A unique name for this Guard. Defaults to `gr-` + the object id.
description (str, optional): A description for this Guard. Defaults to None.
output_formatter (str | Formatter, optional): 'none' (default), 'jsonformer', or a Guardrails Formatter.
""" # noqa

if num_reasks:
warnings.warn(
"Setting num_reasks during initialization is deprecated"
" and will be removed in 0.6.x!"
"We recommend setting num_reasks when calling guard()"
" or guard.parse() instead."
"If you insist on setting it at the Guard level,"
" use 'Guard.configure()'.",
DeprecationWarning,
)

if reask_instructions:
warnings.warn(
"reask_instructions is deprecated and will be removed in 0.6.x!"
"Please be prepared to set reask_messages instead.",
DeprecationWarning,
)
if reask_prompt:
warnings.warn(
"reask_prompt is deprecated and will be removed in 0.6.x!"
"Please be prepared to set reask_messages instead.",
DeprecationWarning,
)

# We have to set the tracer in the ContextStore before the Rail,
# and therefore the Validators, are initialized
cls._set_tracer(cls, tracer) # type: ignore

schema = pydantic_model_to_schema(output_class)
exec_opts = GuardExecutionOptions(
prompt=prompt,
instructions=instructions,
reask_prompt=reask_prompt,
reask_instructions=reask_instructions,
reask_messages=reask_messages,
messages=messages,
)

# TODO: This is the only line that's changed vs the parent Guard class
# Find a way to refactor this
guard = cls(
nemorails=nemorails,
name=name,
description=description,
output_schema=schema.json_schema,
validators=schema.validators,
)
if schema.output_type == OutputTypes.LIST:
guard = cast(Guard[List], guard)
else:
guard = cast(Guard[Dict], guard)
guard.configure(num_reasks=num_reasks, tracer=tracer)
guard._validator_map = schema.validator_map
guard._exec_opts = exec_opts
guard._output_type = schema.output_type
guard._base_model = output_class
if isinstance(output_formatter, str):
if isinstance(output_class, list):
raise Exception("""Root-level arrays are not supported with the
jsonformer argument, but can be used with other json generation methods.
Omit the output_formatter argument to use the other methods.""")
output_formatter = get_formatter(
output_formatter,
schema=output_class.model_json_schema(), # type: ignore
)
guard._output_formatter = output_formatter
guard._fill_validators()
return guard

# create the callable
def _custom_nemo_callable(self, *args, generate_kwargs, **kwargs):
# .generate doesn't like temp
kwargs.pop("temperature", None)

# msg_history, messages, prompt, and instruction all may or may not be present.
# if none of them are present, raise an error
# if messages is present, use that
# if msg_history is present, use

msg_history = kwargs.pop("msg_history", None)
messages = kwargs.pop("messages", None)
prompt = kwargs.pop("prompt", None)
instructions = kwargs.pop("instructions", None)

if msg_history is not None and messages is None:
messages = msg_history

if messages is None and msg_history is None:
messages = []
if instructions is not None:
messages.append({"role": "system", "content": instructions})
if prompt is not None:
messages.append({"role": "system", "content": prompt})

if messages is [] or messages is None:
raise ValueError("messages, prompt, or instructions should be passed during a call.")

# kwargs["messages"] = messages

# return (self._nemorails.generate(**kwargs))["content"] # type: ignore
if not generate_kwargs:
generate_kwargs = {}
return (self._nemorails.generate(messages=messages, **generate_kwargs))["content"] # type: ignore

@deprecated(
"This method has been deprecated. Please use the main constructor `NemoGuardrailsGuard(nemorails=nemorails)` or the `from_pydantic` method.",
)
def from_rail_string(cls, *args, **kwargs):
raise NotImplementedError("""\
`from_rail_string` is not implemented for NemoguardrailsGuard.
We recommend using the main constructor `NemoGuardrailsGuard(nemorails=nemorails)`
or the `from_pydantic` method.""")

@deprecated(
"This method has been deprecated. Please use the main constructor `NemoGuardrailsGuard(nemorails=nemorails)` or the `from_pydantic` method.",
)
def from_rail(cls, *args, **kwargs):
raise NotImplementedError("""\
`from_rail` is not implemented for NemoguardrailsGuard.
We recommend using the main constructor `NemoGuardrailsGuard(nemorails=nemorails)`
or the `from_pydantic` method.""")
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ pillow = "^10.1.0"
cairosvg = "^2.7.1"
mkdocs-glightbox = "^0.3.4"

[tool.poetry.group.nemoguardrials]
optional = true

[tool.poetry.group.nemoguardrials.dependencies]
nemoguardrials = "0.9.1.1"

[[tool.poetry.source]]
name = "PyPI"

Expand Down