Skip to content

Commit dcb3e2b

Browse files
authored
feat: add tests for guardrails builtin detectors (#357)
* feat: add tests for guardrails builtin detectors * fix: comments * fix: missing fstring * feat: add helper function to check detection field of response * feat: add helper function to check detection field of response * fix: unify negative scenarios, add temperature (0.0) to request, improve logging and input msgs * fix: remove unnecessary admin_client from signature and fix docstring spacing
1 parent e7aee74 commit dcb3e2b

File tree

3 files changed

+351
-24
lines changed

3 files changed

+351
-24
lines changed

tests/model_explainability/guardrails/conftest.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from ocp_resources.service import Service
1515
from ocp_resources.serving_runtime import ServingRuntime
1616

17+
from utilities.certificates_utils import create_ca_bundle_file
1718
from utilities.constants import (
1819
KServeDeploymentType,
1920
Labels,
@@ -29,21 +30,6 @@
2930
GUARDRAILS_ORCHESTRATOR_PORT: int = 8032
3031

3132

32-
@pytest.fixture(scope="class")
33-
def guardrails_orchestrator_health_route(
34-
admin_client: DynamicClient,
35-
model_namespace: Namespace,
36-
guardrails_orchestrator: GuardrailsOrchestrator,
37-
) -> Generator[Route, Any, Any]:
38-
route = Route(
39-
name=f"{guardrails_orchestrator.name}-health",
40-
namespace=guardrails_orchestrator.namespace,
41-
wait_for_resource=True,
42-
ensure_exists=True,
43-
)
44-
yield route
45-
46-
4733
@pytest.fixture(scope="class")
4834
def guardrails_orchestrator(
4935
admin_client: DynamicClient,
@@ -67,6 +53,32 @@ def guardrails_orchestrator(
6753
yield gorch
6854

6955

56+
@pytest.fixture(scope="class")
57+
def guardrails_orchestrator_health_route(
58+
admin_client: DynamicClient,
59+
model_namespace: Namespace,
60+
guardrails_orchestrator: GuardrailsOrchestrator,
61+
) -> Generator[Route, Any, Any]:
62+
yield Route(
63+
name=f"{guardrails_orchestrator.name}-health",
64+
namespace=guardrails_orchestrator.namespace,
65+
wait_for_resource=True,
66+
)
67+
68+
69+
@pytest.fixture(scope="class")
70+
def guardrails_orchestrator_route(
71+
admin_client: DynamicClient,
72+
model_namespace: Namespace,
73+
guardrails_orchestrator: GuardrailsOrchestrator,
74+
) -> Generator[Route, Any, Any]:
75+
yield Route(
76+
name=f"{guardrails_orchestrator.name}",
77+
namespace=guardrails_orchestrator.namespace,
78+
wait_for_resource=True,
79+
)
80+
81+
7082
@pytest.fixture(scope="class")
7183
def guardrails_orchestrator_pod(
7284
admin_client: DynamicClient, model_namespace: Namespace, guardrails_orchestrator: GuardrailsOrchestrator
@@ -204,3 +216,10 @@ def guardrails_gateway_config(
204216
},
205217
) as cm:
206218
yield cm
219+
220+
221+
@pytest.fixture(scope="class")
222+
def openshift_ca_bundle_file(
223+
admin_client: DynamicClient,
224+
) -> str:
225+
return create_ca_bundle_file(client=admin_client, ca_type="openshift")

tests/model_explainability/guardrails/test_guardrails.py

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,41 @@
22

33
import pytest
44
import requests
5+
from simple_logger.logger import get_logger
56
from timeout_sampler import retry
67

8+
from tests.model_explainability.guardrails.utils import (
9+
verify_builtin_detector_unsuitable_input_response,
10+
verify_negative_detection_response,
11+
verify_builtin_detector_unsuitable_output_response,
12+
get_auth_headers,
13+
get_chat_payload,
14+
)
715
from tests.model_explainability.utils import validate_tai_component_images
816
from utilities.constants import Timeout, MinIo
17+
from utilities.plugins.constant import OpenAIEnpoints
18+
19+
LOGGER = get_logger(name=__name__)
20+
EXAMPLE_EMAIL_ADDRESS: str = "myemail@domain.com"
21+
INPUT_WITH_EMAIL_ADDRESS: str = f"This is my email address: {EXAMPLE_EMAIL_ADDRESS}, just answer ACK."
22+
PII_ENDPOINT: str = "/pii"
23+
24+
25+
@pytest.mark.parametrize(
26+
"model_namespace",
27+
[
28+
pytest.param(
29+
{"name": "test-guardrails-image"},
30+
)
31+
],
32+
indirect=True,
33+
)
34+
@pytest.mark.smoke
35+
def test_validate_guardrails_orchestrator_images(guardrails_orchestrator_pod, trustyai_operator_configmap):
36+
"""Test to verify Guardrails pod images.
37+
Checks if the image tag from the ConfigMap is used within the Pod and if it's pinned using a sha256 digest.
38+
"""
39+
validate_tai_component_images(pod=guardrails_orchestrator_pod, tai_operator_configmap=trustyai_operator_configmap)
940

1041

1142
@pytest.mark.parametrize(
@@ -21,8 +52,21 @@
2152
)
2253
@pytest.mark.rawdeployment
2354
@pytest.mark.smoke
24-
class TestGuardrailsOrchestrator:
25-
def test_guardrails_health_endpoint(self, admin_client, qwen_isvc, guardrails_orchestrator_health_route):
55+
class TestGuardrailsOrchestratorWithBuiltInDetectors:
56+
"""
57+
Tests that the basic functionality of the GuardrailsOrchestrator work properly with the built-in (regex) detectors.
58+
1. Deploy an LLM using vLLM as a SR.
59+
2. Deploy the Guardrails Orchestrator.
60+
3. Check that the Orchestrator is healthy by querying the health and info endpoints of its /health route.
61+
4. Check that the built-in regex detectors work as expected:
62+
4.1. Unsuitable input detection.
63+
4.2. Unsuitable output detection.
64+
4.3. No detection.
65+
5. Check that the /passthrough endpoint forwards the
66+
query directly to the model without performing any detection.
67+
"""
68+
69+
def test_guardrails_health_endpoint(self, qwen_isvc, guardrails_orchestrator_health_route):
2670
# It takes a bit for the endpoint to come online, so we retry for a brief period of time
2771
@retry(wait_timeout=Timeout.TIMEOUT_1MIN, sleep=1)
2872
def check_health_endpoint():
@@ -34,7 +78,7 @@ def check_health_endpoint():
3478
response = check_health_endpoint()
3579
assert "fms-guardrails-orchestr8" in response.text
3680

37-
def test_guardrails_info_endpoint(self, admin_client, qwen_isvc, guardrails_orchestrator_health_route):
81+
def test_guardrails_info_endpoint(self, qwen_isvc, guardrails_orchestrator_health_route):
3882
response = requests.get(url=f"https://{guardrails_orchestrator_health_route.host}/info", verify=False)
3983
assert response.status_code == http.HTTPStatus.OK
4084

@@ -43,10 +87,66 @@ def test_guardrails_info_endpoint(self, admin_client, qwen_isvc, guardrails_orch
4387
assert response_data["services"]["chat_generation"]["status"] == healthy_status
4488
assert response_data["services"]["regex"]["status"] == healthy_status
4589

46-
def test_validate_guardrails_orchestrator_images(self, guardrails_orchestrator_pod, trustyai_operator_configmap):
47-
"""Test to verify Guardrails pod images.
48-
Checks if the image tag from the ConfigMap is used within the Pod and if it's pinned using a sha256 digest.
49-
"""
50-
validate_tai_component_images(
51-
pod=guardrails_orchestrator_pod, tai_operator_configmap=trustyai_operator_configmap
90+
def test_guardrails_builtin_detectors_unsuitable_input(
91+
self, current_client_token, openshift_ca_bundle_file, guardrails_orchestrator_route
92+
):
93+
response = requests.post(
94+
url=f"https://{guardrails_orchestrator_route.host}{PII_ENDPOINT}{OpenAIEnpoints.CHAT_COMPLETIONS}",
95+
headers=get_auth_headers(token=current_client_token),
96+
json=get_chat_payload(content=INPUT_WITH_EMAIL_ADDRESS),
97+
verify=openshift_ca_bundle_file,
98+
)
99+
100+
verify_builtin_detector_unsuitable_input_response(
101+
response=response,
102+
detector_id="regex",
103+
detection_name="EmailAddress",
104+
detection_type="pii",
105+
detection_text=EXAMPLE_EMAIL_ADDRESS,
106+
)
107+
108+
def test_guardrails_builtin_detectors_unsuitable_output(
109+
self, current_client_token, openshift_ca_bundle_file, guardrails_orchestrator_route
110+
):
111+
response = requests.post(
112+
url=f"https://{guardrails_orchestrator_route.host}{PII_ENDPOINT}{OpenAIEnpoints.CHAT_COMPLETIONS}",
113+
headers=get_auth_headers(token=current_client_token),
114+
json=get_chat_payload(
115+
content="Hi, write three and only three examples of email adresses "
116+
"that I can use to create an account for an online service."
117+
"Don't provide any additional explanation."
118+
),
119+
verify=openshift_ca_bundle_file,
52120
)
121+
122+
verify_builtin_detector_unsuitable_output_response(
123+
response=response, detector_id="regex", detection_name="EmailAddress", detection_type="pii"
124+
)
125+
126+
@pytest.mark.parametrize(
127+
"message, url_path",
128+
[
129+
pytest.param(
130+
"What is the opposite of up?",
131+
PII_ENDPOINT,
132+
id="harmless_input",
133+
),
134+
pytest.param(INPUT_WITH_EMAIL_ADDRESS, "/passthrough", id="pastthrough_endpoint"),
135+
],
136+
)
137+
def test_guardrails_builtin_detectors_negative_detection(
138+
self,
139+
current_client_token,
140+
openshift_ca_bundle_file,
141+
guardrails_orchestrator_route,
142+
message,
143+
url_path,
144+
):
145+
response = requests.post(
146+
url=f"https://{guardrails_orchestrator_route.host}{url_path}{OpenAIEnpoints.CHAT_COMPLETIONS}",
147+
headers=get_auth_headers(token=current_client_token),
148+
json=get_chat_payload(content=str(message)),
149+
verify=openshift_ca_bundle_file,
150+
)
151+
152+
verify_negative_detection_response(response=response)

0 commit comments

Comments
 (0)