Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 2 deletions garak/configurable.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def _load_config(self, config_root=_config):
self._apply_config(plugins_config[namespaced_klass])
self._apply_run_defaults()
self._apply_missing_instance_defaults()
if hasattr(self, "ENV_VAR"):
if hasattr(self, "ENV_VAR") and self.ENV_VAR:
if not hasattr(self, "key_env_var"):
self.key_env_var = self.ENV_VAR
self._validate_env_var()
Expand Down Expand Up @@ -167,7 +167,7 @@ def _apply_missing_instance_defaults(self):
setattr(self, k, v)

def _validate_env_var(self):
if hasattr(self, "key_env_var"):
if hasattr(self, "key_env_var") and self.key_env_var:
if not hasattr(self, "api_key") or self.api_key is None:
self.api_key = os.getenv(self.key_env_var, default=None)
if self.api_key is None:
Expand Down
34 changes: 34 additions & 0 deletions garak/generators/guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from garak import _config
from garak.attempt import Message, Conversation
from garak.generators.base import Generator
from garak.generators.openai import OpenAICompatible


class NeMoGuardrails(Generator):
Expand Down Expand Up @@ -51,4 +52,37 @@ def _call_model(
return [None]


class NeMoGuardrailsServer(OpenAICompatible):
"""Generator for NeMo Guardrails Server

To select specific rails in a multi rail deployment set `config_ids` to match the rail configuration names
as documented by the `NeMo guardrails SDK <https://docs.nvidia.com/nemo/guardrails/0.21.0/run-rails/using-fastapi-server/chat-with-guardrailed-model.html#using-the-openai-python-sdk>`_.
"""

ENV_VAR = None

supports_multiple_generations = False
generator_family_name = "Guardrails"

DEFAULT_PARAMS = OpenAICompatible.DEFAULT_PARAMS | {
"uri": "http://localhost:8000/v1/",
Comment thread
leondz marked this conversation as resolved.
"config_ids": set(),
Comment thread
jmartin-tech marked this conversation as resolved.
Comment thread
jmartin-tech marked this conversation as resolved.
}

def __init__(self, name="", config_root=_config):
self.api_key = "not-used" # suppress any api_key from being sent as the server does not utilize one
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is it worth setting ENV_VAR/key_env_var to None, or deleting ENV_VAR, to reduce potential confusion from this attr being inherited?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Interesting point, currently NeMo Guardrails server does not offer any built in authentication, leading to the suppression added here is matched to the public docs from the sdk. I agree there is some confusion created by keeping the ENV_VAR on the class. I will test other patterns for this suppression.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

ENV_VAR is inherited so I added support for it being None as a suppression of the requirement in Configurable.

A place holder value still needs to be set here to ensure the openai client library does not raise when no key is supplied or found in their standard variable locations. Should we adjust this to apply the place holder after the call to super() to allow a user to provide api_key or key_env_var explicitly in the config file for this generator? This might future proof this better if NeMo Guardrails were to ever add support for injecting an authentication layer on the the hosting server.

super().__init__(name, config_root)
if self.extra_params and not self.extra_params.get("extra_body"):
self.extra_params.append("extra_body")

guardrails = None
if self.config_ids:
guardrails = {"config_ids": self.config_ids}
if guardrails:
if hasattr(self, "extra_body") and self.extra_body and self.config_ids:
self.extra_body["guardrails"] = guardrails
else:
self.extra_body = {"guardrails": guardrails}
Comment on lines +82 to +85
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is mysterious to me. i accept it, and welcome the mystery.

-- is something being worked around here? maybe a brief explanation in the docstring / a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Official docs this was based on here. Will work out how to document this better on the class.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Please validate update in d531028 meets the ask here.



DEFAULT_CLASS = "NeMoGuardrails"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this still a sensible default?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I would be in favor of shifting if there is consensus. That would elevate this to a breaking change for this addition as configurations that did not specify -t guardrails.NeMoGuardrails would change behavior.

7 changes: 6 additions & 1 deletion tests/generators/test_generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,12 @@ def test_instantiate_generators(classname):
NON_CONVERSATION_GENERATORS = [
classname
for classname in GENERATORS
if not ("openai" in classname or "groq" in classname or "azure" in classname)
if not (
"openai" in classname
or "groq" in classname
or "azure" in classname
or "NeMoGuardrailsServer" in classname
)
]


Expand Down
53 changes: 53 additions & 0 deletions tests/generators/test_guardrails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import httpx
import pytest

from garak.attempt import Message, Turn, Conversation
from garak.generators.guardrails import NeMoGuardrailsServer


def guardrails_config(selected_rails):
"""helper method to provide generator configuration"""
return {
"generators": {
"guardrails": {
"NeMoGuardrailsServer": {
"name": "UnknownModel",
"config_ids": selected_rails,
}
}
}
}


@pytest.mark.parametrize(
"selected_rails",
[
[],
["rail1"],
["rail1", "rail2"],
],
)
@pytest.mark.respx(base_url=NeMoGuardrailsServer.DEFAULT_PARAMS["uri"])
def test_guardrail_selection(selected_rails, respx_mock, openai_compat_mocks):
"""validate selected rails are passed as headers on the request"""
mock_response = openai_compat_mocks["chat"]
mock_request = respx_mock.post("chat/completions")
mock_request.mock(
return_value=httpx.Response(
mock_response["code"],
json=mock_response["json"],
)
)
config_root = guardrails_config(selected_rails)
g = NeMoGuardrailsServer(config_root=config_root)
conv = Conversation(turns=[Turn(role="user", content=Message("Testing text"))])
g.generate(conv)
assert mock_request.called
for rail in selected_rails:
content = str(mock_request.calls.last.request.content)
assert "guardrails" in content
assert "config_ids" in content
assert rail in content
Loading