import json
from typing import Any, Dict
import pytest
from pydantic import BaseModel, Field
from guardrails import Guard, Validator, register_validator
from guardrails.async_guard import AsyncGuard
from guardrails.errors import ValidationError
from guardrails.actions.reask import FieldReAsk
from guardrails.actions.refrain import Refrain
from guardrails.actions.filter import Filter
from guardrails_ai.types import (
FailResult,
PassResult,
ValidationResult,
)
from guardrails.types import OnFailAction
from tests.integration_tests.test_assets.validators import (
TwoWords,
ValidLength,
)
@register_validator("mycustomhellovalidator", data_type="string")
def hello_validator(value: Any, metadata: Dict[str, Any]) -> ValidationResult:
if "hello" in value.lower():
return FailResult(
error_message="Hello is too basic, try something more creative.",
fix_value="hullo",
)
return PassResult()
def test_validator_as_tuple():
# (Callable, on_fail) tuple fix
class MyModel(BaseModel):
a_field: str = Field(..., validators=[(hello_validator(), OnFailAction.FIX)])
guard = Guard.for_pydantic(MyModel)
output = guard.parse(
'{"a_field": "hello there yo"}',
num_reasks=0,
)
assert output.validated_output == {"a_field": "hullo"}
# (string, on_fail) tuple fix
class MyModel(BaseModel):
a_field: str = Field(
...,
validators=[
("two_words", OnFailAction.REASK),
("mycustomhellovalidator", OnFailAction.FIX),
],
)
guard = Guard.for_pydantic(MyModel)
output = guard.parse(
'{"a_field": "hello there yo"}',
num_reasks=0,
)
assert output.validated_output == {"a_field": "hullo"}
# (Validator, on_fail) tuple fix
class MyModel(BaseModel):
a_field: str = Field(..., validators=[(TwoWords(), OnFailAction.FIX)])
guard = Guard.for_pydantic(MyModel)
output = guard.parse(
'{"a_field": "hello there yo"}',
num_reasks=0,
)
assert output.validated_output == {"a_field": "hello there"}
# (Validator, on_fail) tuple reask
hullo_reask = FieldReAsk(
incorrect_value="hello there yo",
fail_results=[
FailResult(
error_message="Hello is too basic, try something more creative.",
fix_value="hullo",
)
],
path=["a_field"],
)
class MyModel(BaseModel):
a_field: str = Field(..., validators=[(hello_validator(), OnFailAction.REASK)])
guard = Guard.for_pydantic(MyModel)
output = guard.parse(
'{"a_field": "hello there yo"}',
num_reasks=0,
)
assert output.validated_output == {"a_field": "hullo"}
assert guard.history.first.iterations.first.reasks[0] == hullo_reask
hello_reask = FieldReAsk(
incorrect_value="hello there yo",
fail_results=[
FailResult(
error_message="must be exactly two words",
fix_value="hello there",
)
],
path=["a_field"],
)
# (string, on_fail) tuple reask
class MyModel(BaseModel):
a_field: str = Field(..., validators=[("two-words", OnFailAction.REASK)])
guard = Guard.for_pydantic(MyModel)
output = guard.parse(
'{"a_field": "hello there yo"}',
num_reasks=0,
)
assert output.validated_output == {"a_field": "hello there"}
assert guard.history.first.iterations.first.reasks[0] == hello_reask
# (Validator, on_fail) tuple reask
class MyModel(BaseModel):
a_field: str = Field(..., validators=[(TwoWords(), OnFailAction.REASK)])
guard = Guard.for_pydantic(MyModel)
output = guard.parse(
'{"a_field": "hello there yo"}',
num_reasks=0,
)
assert output.validated_output == {"a_field": "hello there"}
assert guard.history.first.iterations.first.reasks[0] == hello_reask
class MyModel(BaseModel):
a_field: str = Field(..., validators=["two-words"])
# Unintentionally supported, but supported nonetheless
# with pytest.raises(ValueError):
guard = Guard.for_pydantic(MyModel)
assert len(guard._validators) == 1
def test_custom_func_validator():
rail_str = """
"""
guard = Guard.for_rail_string(rail_str)
output = guard.parse(
'{"greeting": "hello"}',
num_reasks=0,
)
assert output.validated_output == {"greeting": "hullo"}
call = guard.history.first
assert call.iterations.length == 1
validator_log = call.iterations.first.validator_logs[0]
assert validator_log.validator_name == "mycustomhellovalidator"
assert validator_log.validation_result == FailResult(
error_message="Hello is too basic, try something more creative.",
fix_value="hullo",
)
def test_bad_validator():
with pytest.raises(ValueError):
@register_validator("mycustombadvalidator", data_type="string")
def validate(value: Any) -> ValidationResult:
pass
@pytest.mark.parametrize(
"min,max,expected_xml",
[
(0, 12, "length: 0 12"),
("0", "12", "length: 0 12"),
(None, 12, "length: None 12"),
(1, None, "length: 1 None"),
],
)
def test_to_xml_attrib(min, max, expected_xml):
validator = ValidLength(min=min, max=max)
xml_validator = validator.to_xml_attrib()
assert xml_validator == expected_xml
def custom_fix_on_fail_handler(value: Any, fail_result: FailResult):
return value + " " + value
def custom_reask_on_fail_handler(value: Any, fail_result: FailResult):
return FieldReAsk(incorrect_value=value, fail_results=[fail_result])
def custom_exception_on_fail_handler(value: Any, fail_result: FailResult):
raise ValidationError("Something went wrong!")
def custom_filter_on_fail_handler(value: Any, fail_result: FailResult):
return Filter()
def custom_refrain_on_fail_handler(value: Any, fail_result: FailResult):
return Refrain()
class TestCustomOnFailHandler:
def test_custom_fix(self):
prompt = """
What kind of pet should I get and what should I name it?
${gr.complete_json_suffix_v2}
"""
messages = [
{
"role": "user",
"content": prompt,
}
]
output = """
{
"pet_type": "dog",
"name": "Fido"
}
"""
expected_result = {"pet_type": "dog dog", "name": "Fido"}
validator: Validator = TwoWords(on_fail=custom_fix_on_fail_handler)
class Pet(BaseModel):
pet_type: str = Field(description="Species of pet", validators=[validator])
name: str = Field(description="a unique pet name")
guard = Guard.for_pydantic(output_class=Pet, messages=messages)
response = guard.parse(output, num_reasks=0)
assert response.validation_passed is True
assert response.validated_output == expected_result
def test_custom_reask(self):
prompt = """
What kind of pet should I get and what should I name it?
${gr.complete_json_suffix_v2}
"""
messages = [
{
"role": "user",
"content": prompt,
}
]
output = """
{
"pet_type": "dog",
"name": "Fido"
}
"""
expected_result = FieldReAsk(
incorrect_value="dog",
path=["pet_type"],
fail_results=[
FailResult(
error_message="must be exactly two words",
fix_value="dog dog",
)
],
)
validator: Validator = TwoWords(on_fail=custom_reask_on_fail_handler)
class Pet(BaseModel):
pet_type: str = Field(description="Species of pet", validators=[validator])
name: str = Field(description="a unique pet name")
guard = Guard.for_pydantic(output_class=Pet, messages=messages)
response = guard.parse(output, num_reasks=0)
# Why? Because we have a bad habit of applying every fix value
# to the output even if the user doesn't ask us to.
assert response.validation_passed is True
assert guard.history.first.iterations.first.reasks[0] == expected_result
def test_custom_exception(self):
prompt = """
What kind of pet should I get and what should I name it?
${gr.complete_json_suffix_v2}
"""
messages = [
{
"role": "user",
"content": prompt,
}
]
output = """
{
"pet_type": "dog",
"name": "Fido"
}
"""
validator: Validator = TwoWords(on_fail=custom_exception_on_fail_handler)
class Pet(BaseModel):
pet_type: str = Field(description="Species of pet", validators=[validator])
name: str = Field(description="a unique pet name")
guard = Guard.for_pydantic(output_class=Pet, messages=messages)
with pytest.raises(ValidationError) as excinfo:
guard.parse(output, num_reasks=0)
assert str(excinfo.value) == "Something went wrong!"
def test_custom_filter(self):
prompt = """
What kind of pet should I get and what should I name it?
${gr.complete_json_suffix_v2}
"""
messages = [
{
"role": "user",
"content": prompt,
}
]
output = """
{
"pet_type": "dog",
"name": "Fido"
}
"""
validator: Validator = TwoWords(on_fail=custom_filter_on_fail_handler)
class Pet(BaseModel):
pet_type: str = Field(description="Species of pet", validators=[validator])
name: str = Field(description="a unique pet name")
guard = Guard.for_pydantic(output_class=Pet, messages=messages)
response = guard.parse(output, num_reasks=0)
# NOTE: This doesn't seem right.
# Shouldn't pass if filtering is successful on the target property?
assert response.validation_passed is False
assert response.validated_output is None
def test_custom_refrain(self):
prompt = """
What kind of pet should I get and what should I name it?
${gr.complete_json_suffix_v2}
"""
messages = [
{
"role": "user",
"content": prompt,
}
]
output = """
{
"pet_type": "dog",
"name": "Fido"
}
"""
validator: Validator = TwoWords(on_fail=custom_refrain_on_fail_handler)
class Pet(BaseModel):
pet_type: str = Field(description="Species of pet", validators=[validator])
name: str = Field(description="a unique pet name")
guard = Guard.for_pydantic(output_class=Pet, messages=messages)
response = guard.parse(output, num_reasks=0)
assert response.validation_passed is False
assert response.validated_output is None
class Pet(BaseModel):
name: str = Field(description="a unique pet name")
def test_input_validation_fix(mocker):
def mock_llm_api(messages, *args, **kwargs):
return json.dumps({"name": "Fluffy"})
# fix returns an amended value for prompt/instructions validation,
guard = Guard.for_pydantic(output_class=Pet)
guard.use(TwoWords(on_fail=OnFailAction.FIX), on="messages")
guard(
mock_llm_api,
messages=[
{
"role": "user",
"content": "What kind of pet should I get?",
}
],
)
assert (
guard.history.first.iterations.first.outputs.validation_response == "What kind"
)
# but raises for messages validation
guard = Guard.for_pydantic(output_class=Pet)
guard.use(TwoWords(on_fail=OnFailAction.EXCEPTION), on="messages")
with pytest.raises(ValidationError) as excinfo:
guard(
mock_llm_api,
messages=[
{
"role": "user",
"content": "What kind of pet should I get?",
}
],
)
assert str(excinfo.value) == (
"Validation failed for field with errors: must be exactly two words"
)
assert isinstance(guard.history.first.exception, ValidationError)
assert guard.history.first.exception == excinfo.value
# rail messages validation
guard = Guard.for_rail_string(
"""
This is not two words
This also is not two words
"""
)
guard(
mock_llm_api,
)
assert (
guard.history.first.iterations.first.outputs.validation_response == "This also"
)
@pytest.mark.asyncio
async def test_async_messages_validation_fix(mocker):
async def mock_llm_api(messages, *args, **kwargs) -> str:
return json.dumps({"name": "Fluffy"})
# fix returns an amended value for messages validation,
guard = AsyncGuard.for_pydantic(output_class=Pet)
guard.use(TwoWords(on_fail=OnFailAction.FIX), on="messages")
await guard(
mock_llm_api,
messages=[
{
"role": "user",
"content": "What kind of pet should I get?",
}
],
)
assert (
guard.history.first.iterations.first.outputs.validation_response == "What kind"
)
guard = AsyncGuard.for_pydantic(output_class=Pet)
guard.use(TwoWords(on_fail=OnFailAction.FIX), on="messages")
await guard(
mock_llm_api,
messages=[
{
"role": "user",
"content": "But really, what kind of pet should I get?",
}
],
)
assert (
guard.history.first.iterations.first.outputs.validation_response
== "But really,"
)
# but raises for messages validation
guard = AsyncGuard.for_pydantic(output_class=Pet)
guard.use(TwoWords(on_fail=OnFailAction.FIX), on="messages")
await guard(
mock_llm_api,
messages=[
{
"role": "user",
"content": "What kind of pet should I get?",
}
],
)
first_iter = guard.history.first.iterations.first
assert first_iter.outputs.validation_response == "What kind"
# rail prompt validation
guard = AsyncGuard.for_rail_string(
"""
This is not two words
"""
)
await guard(
mock_llm_api,
)
assert guard.history.first.iterations.first.outputs.validation_response == "This is"
@pytest.mark.parametrize(
"on_fail,structured_messages_error,unstructured_messages_error,",
[
(
OnFailAction.REASK,
"Messages validation failed: incorrect_value='What kind of pet should I get?' fail_results=[FailResult(outcome=, metadata=None, validated_chunk=None, error_message='must be exactly two words', fix_value='What kind', error_spans=None)] path=None", # noqa
"Messages validation failed: incorrect_value='What kind of pet should I get?' fail_results=[FailResult(outcome=, metadata=None, validated_chunk=None, error_message='must be exactly two words', fix_value='What kind', error_spans=None)] path=None", # noqa
),
(
OnFailAction.FILTER,
"Messages validation failed",
"Messages validation failed",
),
(
OnFailAction.REFRAIN,
"Messages validation failed",
"Messages validation failed",
),
(
OnFailAction.EXCEPTION,
"Validation failed for field with errors: must be exactly two words",
"Validation failed for field with errors: must be exactly two words",
),
],
)
def test_input_validation_fail(
on_fail,
structured_messages_error,
unstructured_messages_error,
):
# With Prompt Validation
guard = Guard.for_pydantic(output_class=Pet)
guard.use(TwoWords(on_fail=on_fail), on="messages")
def custom_llm(messages, *args, **kwargs):
raise Exception(
"LLM was called when it should not have been!"
"Input Validation did not raise as expected!"
)
# With messages Validation
guard = Guard.for_pydantic(output_class=Pet)
guard.use(TwoWords(on_fail=on_fail), on="messages")
with pytest.raises(ValidationError) as excinfo:
guard(
custom_llm,
messages=[
{
"role": "user",
"content": "What kind of pet should I get?",
}
],
)
assert str(excinfo.value) == structured_messages_error
assert isinstance(guard.history.last.exception, ValidationError)
assert guard.history.last.exception == excinfo.value
# Rail Prompt Validation
guard = Guard.for_rail_string(
f"""
What kind of pet should I get?
"""
)
with pytest.raises(ValidationError) as excinfo:
guard(
custom_llm,
)
assert str(excinfo.value) == unstructured_messages_error
assert isinstance(guard.history.last.exception, ValidationError)
assert guard.history.last.exception == excinfo.value
@pytest.mark.parametrize(
"on_fail,structured_messages_error,unstructured_messages_error,",
[
(
OnFailAction.REASK,
"Messages validation failed: incorrect_value='What kind of pet should I get?' fail_results=[FailResult(outcome=, metadata=None, validated_chunk=None, error_message='must be exactly two words', fix_value='What kind', error_spans=None)] path=None", # noqa
"Messages validation failed: incorrect_value='What kind of pet should I get?' fail_results=[FailResult(outcome=, metadata=None, validated_chunk=None, error_message='must be exactly two words', fix_value='What kind', error_spans=None)] path=None", # noqa
),
(
OnFailAction.FILTER,
"Messages validation failed",
"Messages validation failed",
),
(
OnFailAction.REFRAIN,
"Messages validation failed",
"Messages validation failed",
),
(
OnFailAction.EXCEPTION,
"Validation failed for field with errors: must be exactly two words",
"Validation failed for field with errors: must be exactly two words",
),
],
)
@pytest.mark.asyncio
async def test_input_validation_fail_async(
mocker,
on_fail,
structured_messages_error,
unstructured_messages_error,
):
async def custom_llm(messages, *args, **kwargs) -> str:
raise Exception(
"LLM was called when it should not have been!"
"Input Validation did not raise as expected!"
)
# with_messages_validation
guard = AsyncGuard.for_pydantic(output_class=Pet)
guard.use(TwoWords(on_fail=on_fail), on="messages")
with pytest.raises(ValidationError) as excinfo:
await guard(
custom_llm,
messages=[
{
"role": "user",
"content": "What kind of pet should I get?",
}
],
)
assert str(excinfo.value) == structured_messages_error
assert isinstance(guard.history.last.exception, ValidationError)
assert guard.history.last.exception == excinfo.value
# with_messages_validation
guard = AsyncGuard.for_pydantic(output_class=Pet)
guard.use(TwoWords(on_fail=on_fail), on="messages")
with pytest.raises(ValidationError) as excinfo:
await guard(
custom_llm,
messages=[
{
"role": "user",
"content": "What kind of pet should I get?",
}
],
)
assert str(excinfo.value) == structured_messages_error
assert isinstance(guard.history.last.exception, ValidationError)
assert guard.history.last.exception == excinfo.value
# rail prompt validation
guard = AsyncGuard.for_rail_string(
f"""
What kind of pet should I get?
"""
)
with pytest.raises(ValidationError) as excinfo:
await guard(
custom_llm,
)
assert str(excinfo.value) == unstructured_messages_error
assert isinstance(guard.history.last.exception, ValidationError)
assert guard.history.last.exception == excinfo.value