참고소스 수정본
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
from tests.integration_tests.test_assets.validators.ends_with import EndsWith
|
||||
from tests.integration_tests.test_assets.validators.lower_case import LowerCase
|
||||
from tests.integration_tests.test_assets.validators.one_line import OneLine
|
||||
from tests.integration_tests.test_assets.validators.reading_time import ReadingTime
|
||||
from tests.integration_tests.test_assets.validators.regex_match import RegexMatch
|
||||
from tests.integration_tests.test_assets.validators.two_words import TwoWords
|
||||
from tests.integration_tests.test_assets.validators.upper_case import UpperCase
|
||||
from tests.integration_tests.test_assets.validators.valid_choices import ValidChoices
|
||||
from tests.integration_tests.test_assets.validators.valid_length import ValidLength
|
||||
from tests.integration_tests.test_assets.validators.valid_url import ValidURL
|
||||
from tests.integration_tests.test_assets.validators.detect_pii import MockDetectPII
|
||||
|
||||
__all__ = [
|
||||
"EndsWith",
|
||||
"LowerCase",
|
||||
"OneLine",
|
||||
"ReadingTime",
|
||||
"RegexMatch",
|
||||
"TwoWords",
|
||||
"UpperCase",
|
||||
"ValidChoices",
|
||||
"ValidLength",
|
||||
"ValidURL",
|
||||
"MockDetectPII",
|
||||
]
|
||||
@@ -0,0 +1,187 @@
|
||||
from typing import Any, Callable, Dict, List, Union
|
||||
import difflib
|
||||
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
from guardrails.validator_base import ErrorSpan
|
||||
|
||||
|
||||
@register_validator(name="guardrails/detect_pii", data_type="string")
|
||||
class MockDetectPII(Validator):
|
||||
"""Validates that any text does not contain any PII.
|
||||
|
||||
Instead of using Microsoft Presidio, it accepts a map of PII
|
||||
text to their replacements, and performs a simple string replacement.
|
||||
For example, if the map is {"John Doe": "REDACTED"}, then the text "John
|
||||
Doe is a person" will be replaced with "REDACTED is a person".
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | --------------------------------------- |
|
||||
| Name for `format` attribute | `pii` |
|
||||
| Supported data types | `string` |
|
||||
| Programmatic fix | Anonymized text with PII filtered out |
|
||||
|
||||
Args:
|
||||
pii_entities (str | List[str], optional): The PII entities to filter. Must be
|
||||
one of `pii` or `spi`. Defaults to None. Can also be set in metadata.
|
||||
"""
|
||||
|
||||
PII_ENTITIES_MAP = {
|
||||
"pii": [
|
||||
"EMAIL_ADDRESS",
|
||||
"PHONE_NUMBER",
|
||||
"DOMAIN_NAME",
|
||||
"IP_ADDRESS",
|
||||
"DATE_TIME",
|
||||
"LOCATION",
|
||||
"PERSON",
|
||||
"URL",
|
||||
],
|
||||
"spi": [
|
||||
"CREDIT_CARD",
|
||||
"CRYPTO",
|
||||
"IBAN_CODE",
|
||||
"NRP",
|
||||
"MEDICAL_LICENSE",
|
||||
"US_BANK_NUMBER",
|
||||
"US_DRIVER_LICENSE",
|
||||
"US_ITIN",
|
||||
"US_PASSPORT",
|
||||
"US_SSN",
|
||||
],
|
||||
}
|
||||
|
||||
def chunking_function(self, chunk: str):
|
||||
"""Use a sentence tokenizer to split the chunk into sentences.
|
||||
|
||||
Because using the tokenizer is expensive, we only use it if
|
||||
there is a period present in the chunk.
|
||||
"""
|
||||
try:
|
||||
import nltk
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"nltk is required for sentence splitting. Please install it using "
|
||||
"`poetry add nltk`"
|
||||
)
|
||||
|
||||
# using the sentence tokenizer is expensive
|
||||
# we check for a . to avoid wastefully calling the tokenizer
|
||||
if "." not in chunk:
|
||||
return []
|
||||
sentences = nltk.sent_tokenize(chunk)
|
||||
if len(sentences) == 0:
|
||||
return []
|
||||
if len(sentences) == 1:
|
||||
sentence = sentences[0].strip()
|
||||
# this can still fail if the last chunk ends on the . in an email address
|
||||
if sentence[-1] == ".":
|
||||
return [sentence, ""]
|
||||
else:
|
||||
return []
|
||||
|
||||
# return the sentence
|
||||
# then the remaining chunks that aren't finished accumulating
|
||||
return [sentences[0], "".join(sentences[1:])]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pii_entities: Union[str, List[str], None] = None,
|
||||
on_fail: Union[Callable[..., Any], None] = None,
|
||||
replace_map: Dict[str, str] = {},
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(on_fail, pii_entities=pii_entities, **kwargs)
|
||||
self.pii_entities = pii_entities
|
||||
self.replace_map = replace_map
|
||||
|
||||
def get_anonymized_text(self, text: str, entities: List[str]) -> str:
|
||||
"""Analyze and anonymize the text for PII.
|
||||
|
||||
Args:
|
||||
text (str): The text to analyze.
|
||||
pii_entities (List[str]): The PII entities to filter.
|
||||
|
||||
Returns:
|
||||
anonymized_text (str): The anonymized text.
|
||||
"""
|
||||
anonymized_text = text
|
||||
# iterate through keys in replace_map
|
||||
for key in self.replace_map:
|
||||
anonymized_text = anonymized_text.replace(key, self.replace_map[key])
|
||||
return anonymized_text
|
||||
|
||||
def validate(self, value: Any, metadata: Dict[str, Any]) -> ValidationResult:
|
||||
# Entities to filter passed through metadata take precedence
|
||||
pii_entities = metadata.get("pii_entities", self.pii_entities)
|
||||
if pii_entities is None:
|
||||
raise ValueError(
|
||||
"`pii_entities` must be set in order to use the `DetectPII` validator."
|
||||
"Add this: `pii_entities=['PERSON', 'PHONE_NUMBER']`"
|
||||
"OR pii_entities='pii' or 'spi'"
|
||||
"in init or metadata."
|
||||
)
|
||||
|
||||
pii_keys = list(self.PII_ENTITIES_MAP.keys())
|
||||
# Check that pii_entities is a string OR list of strings
|
||||
if isinstance(pii_entities, str):
|
||||
# A key to the PII_ENTITIES_MAP
|
||||
entities_to_filter = self.PII_ENTITIES_MAP.get(pii_entities, None)
|
||||
if entities_to_filter is None:
|
||||
raise ValueError(f"`pii_entities` must be one of {pii_keys}")
|
||||
elif isinstance(pii_entities, list):
|
||||
entities_to_filter = pii_entities
|
||||
else:
|
||||
raise ValueError(
|
||||
f"`pii_entities` must be one of {pii_keys} or a list of strings."
|
||||
)
|
||||
|
||||
# Analyze the text, and anonymize it if there is PII
|
||||
anonymized_text = self.get_anonymized_text(
|
||||
text=value, entities=entities_to_filter
|
||||
)
|
||||
if anonymized_text == value:
|
||||
return PassResult()
|
||||
|
||||
# TODO: this should be refactored into a helper method in OSS
|
||||
# get character indices of differences between two strings
|
||||
differ = difflib.Differ()
|
||||
diffs = list(differ.compare(value, anonymized_text))
|
||||
start_range = None
|
||||
diff_ranges = []
|
||||
# needs to be tracked separately
|
||||
curr_index_in_original = 0
|
||||
for i in range(len(diffs)):
|
||||
if start_range is not None and diffs[i][0] != "-":
|
||||
diff_ranges.append((start_range, curr_index_in_original))
|
||||
start_range = None
|
||||
if diffs[i][0] == "-":
|
||||
if start_range is None:
|
||||
start_range = curr_index_in_original
|
||||
if diffs[i][0] != "+":
|
||||
curr_index_in_original += 1
|
||||
|
||||
error_spans = []
|
||||
for diff_range in diff_ranges:
|
||||
error_spans.append(
|
||||
ErrorSpan(
|
||||
start=diff_range[0],
|
||||
end=diff_range[1],
|
||||
reason=f"PII detected in {value[diff_range[0] : diff_range[1]]}",
|
||||
)
|
||||
)
|
||||
|
||||
# If anonymized value text is different from original value, then there is PII
|
||||
error_msg = f"The following text in your response contains PII:\n{value}"
|
||||
return FailResult(
|
||||
error_message=(error_msg),
|
||||
fix_value=anonymized_text,
|
||||
error_spans=error_spans,
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from guardrails.logger import logger
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
OnFailAction,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
|
||||
|
||||
@register_validator(name="ends-with", data_type="list")
|
||||
class EndsWith(Validator):
|
||||
"""Validates that a list ends with a given value.
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | --------------------------------- |
|
||||
| Name for `format` attribute | `ends-with` |
|
||||
| Supported data types | `list` |
|
||||
| Programmatic fix | Append the given value to the list. |
|
||||
|
||||
Args:
|
||||
end: The required last element.
|
||||
"""
|
||||
|
||||
def __init__(self, end: str, on_fail: OnFailAction = OnFailAction.FIX):
|
||||
super().__init__(
|
||||
on_fail=on_fail,
|
||||
end=end,
|
||||
)
|
||||
self._end = end
|
||||
|
||||
def validate(self, value: Any, metadata: Dict) -> ValidationResult:
|
||||
logger.debug(f"Validating {value} ends with {self._end}...")
|
||||
|
||||
if not value[-1] == self._end:
|
||||
return FailResult(
|
||||
error_message=f"{value} must end with {self._end}",
|
||||
fix_value=value + [self._end],
|
||||
)
|
||||
|
||||
return PassResult()
|
||||
@@ -0,0 +1,35 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from guardrails.logger import logger
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
|
||||
|
||||
@register_validator(name="lower-case", data_type="string")
|
||||
class LowerCase(Validator):
|
||||
"""Validates that a value is lower case.
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | --------------------------------- |
|
||||
| Name for `format` attribute | `lower-case` |
|
||||
| Supported data types | `string` |
|
||||
| Programmatic fix | Convert to lower case. |
|
||||
"""
|
||||
|
||||
def validate(self, value: Any, metadata: Dict) -> ValidationResult:
|
||||
logger.debug(f"Validating {value} is lower case...")
|
||||
|
||||
if value.lower() != value:
|
||||
return FailResult(
|
||||
error_message=f"Value {value} is not lower case.",
|
||||
fix_value=value.lower(),
|
||||
)
|
||||
|
||||
return PassResult()
|
||||
@@ -0,0 +1,36 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from guardrails.logger import logger
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
|
||||
|
||||
@register_validator(name="one-line", data_type="string")
|
||||
class OneLine(Validator):
|
||||
"""Validates that a value is a single line, based on whether or not the
|
||||
output has a newline character (\\n).
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | -------------------------------------- |
|
||||
| Name for `format` attribute | `one-line` |
|
||||
| Supported data types | `string` |
|
||||
| Programmatic fix | Keep the first line, delete other text |
|
||||
"""
|
||||
|
||||
def validate(self, value: Any, metadata: Dict) -> ValidationResult:
|
||||
logger.debug(f"Validating {value} is a single line...")
|
||||
|
||||
if len(value.splitlines()) > 1:
|
||||
return FailResult(
|
||||
error_message=f"Value {value} is not a single line.",
|
||||
fix_value=value.splitlines()[0],
|
||||
)
|
||||
|
||||
return PassResult()
|
||||
@@ -0,0 +1,54 @@
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
from guardrails.logger import logger
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
|
||||
|
||||
@register_validator(name="reading-time", data_type="string")
|
||||
class ReadingTime(Validator):
|
||||
"""Validates that the a string can be read in less than a certain amount of
|
||||
time.
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | ----------------------------------- |
|
||||
| Name for `format` attribute | `reading-time` |
|
||||
| Supported data types | `string` |
|
||||
| Programmatic fix | None |
|
||||
|
||||
Args:
|
||||
|
||||
reading_time: The maximum reading time in minutes.
|
||||
"""
|
||||
|
||||
def __init__(self, reading_time: int, on_fail: Optional[Callable] = None):
|
||||
super().__init__(
|
||||
on_fail=on_fail,
|
||||
reading_time=reading_time,
|
||||
)
|
||||
self._max_time = reading_time
|
||||
|
||||
def validate(self, value: Any, metadata: Dict) -> ValidationResult:
|
||||
logger.debug(
|
||||
f"Validating {value} can be read in less than {self._max_time} minutes..."
|
||||
)
|
||||
|
||||
# Estimate the reading time of the string
|
||||
reading_time = len(value.split()) / 200
|
||||
logger.debug(f"Estimated reading time {reading_time} minutes...")
|
||||
|
||||
if (reading_time - self._max_time) > 0:
|
||||
logger.error(f"{value} took {reading_time} minutes to read")
|
||||
return FailResult(
|
||||
error_message=f"String should be readable "
|
||||
f"within {self._max_time} minutes."
|
||||
)
|
||||
|
||||
return PassResult()
|
||||
@@ -0,0 +1,74 @@
|
||||
import re
|
||||
import string
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
import rstr
|
||||
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
|
||||
|
||||
@register_validator(name="regex_match", data_type="string")
|
||||
class RegexMatch(Validator):
|
||||
"""Validates that a value matches a regular expression.
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | --------------------------------- |
|
||||
| Name for `format` attribute | `regex_match` |
|
||||
| Supported data types | `string` |
|
||||
| Programmatic fix | Generate a string that matches the regular expression |
|
||||
|
||||
Args:
|
||||
regex: Str regex pattern
|
||||
match_type: Str in {"search", "fullmatch"} for a regex search or full-match option
|
||||
""" # noqa
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
regex: str,
|
||||
match_type: Optional[str] = None,
|
||||
on_fail: Optional[Callable] = None,
|
||||
):
|
||||
# todo -> something forces this to be passed as kwargs and therefore xml-ized.
|
||||
# match_types = ["fullmatch", "search"]
|
||||
if match_type is None:
|
||||
match_type = "fullmatch"
|
||||
assert match_type in [
|
||||
"fullmatch",
|
||||
"search",
|
||||
], 'match_type must be in ["fullmatch", "search"]'
|
||||
|
||||
super().__init__(
|
||||
on_fail=on_fail,
|
||||
match_type=match_type,
|
||||
regex=regex,
|
||||
)
|
||||
self._regex = regex
|
||||
self._match_type = match_type
|
||||
|
||||
def validate(self, value: Any, metadata: Dict) -> ValidationResult:
|
||||
p = re.compile(self._regex)
|
||||
"""Validates that value matches the provided regular expression."""
|
||||
# Pad matching string on either side for fix
|
||||
# example if we are performing a regex search
|
||||
str_padding = (
|
||||
"" if self._match_type == "fullmatch" else rstr.rstr(string.ascii_lowercase)
|
||||
)
|
||||
self._fix_str = str_padding + rstr.xeger(self._regex) + str_padding
|
||||
|
||||
if not getattr(p, self._match_type)(value):
|
||||
return FailResult(
|
||||
errorMessage=f"Result must match {self._regex}",
|
||||
fixValue=self._fix_str,
|
||||
)
|
||||
return PassResult()
|
||||
|
||||
def to_prompt(self, with_keywords: bool = True) -> str:
|
||||
return "results should match " + self._regex
|
||||
@@ -0,0 +1,48 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from pydash.strings import words as _words
|
||||
|
||||
from guardrails.logger import logger
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
|
||||
|
||||
@register_validator(name="two-words", data_type="string")
|
||||
class TwoWords(Validator):
|
||||
"""Validates that a value is two words.
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | --------------------------------- |
|
||||
| Name for `format` attribute | `two-words` |
|
||||
| Supported data types | `string` |
|
||||
| Programmatic fix | Pick the first two words. |
|
||||
"""
|
||||
|
||||
def _get_fix_value(self, value: str) -> str:
|
||||
words = value.split()
|
||||
if len(words) == 1:
|
||||
words = _words(value)
|
||||
|
||||
if len(words) == 1:
|
||||
value = f"{value} {value}"
|
||||
words = value.split()
|
||||
|
||||
return " ".join(words[:2])
|
||||
|
||||
def validate(self, value: Any, metadata: Dict) -> ValidationResult:
|
||||
logger.debug(f"Validating {value} is two words...")
|
||||
|
||||
if len(value.split()) != 2:
|
||||
return FailResult(
|
||||
error_message="must be exactly two words",
|
||||
fix_value=self._get_fix_value(str(value)),
|
||||
)
|
||||
|
||||
return PassResult()
|
||||
@@ -0,0 +1,35 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from guardrails.logger import logger
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
|
||||
|
||||
@register_validator(name="upper-case", data_type="string")
|
||||
class UpperCase(Validator):
|
||||
"""Validates that a value is upper case.
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | --------------------------------- |
|
||||
| Name for `format` attribute | `upper-case` |
|
||||
| Supported data types | `string` |
|
||||
| Programmatic fix | Convert to upper case. |
|
||||
"""
|
||||
|
||||
def validate(self, value: Any, metadata: Dict) -> ValidationResult:
|
||||
logger.debug(f"Validating {value} is upper case...")
|
||||
|
||||
if value.upper() != value:
|
||||
return FailResult(
|
||||
error_message=f"Value {value} is not upper case.",
|
||||
fix_value=value.upper(),
|
||||
)
|
||||
|
||||
return PassResult()
|
||||
@@ -0,0 +1,45 @@
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from guardrails.logger import logger
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
|
||||
|
||||
@register_validator(name="valid-choices", data_type="all")
|
||||
class ValidChoices(Validator):
|
||||
"""Validates that a value is within the acceptable choices.
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | --------------------------------- |
|
||||
| Name for `format` attribute | `valid-choices` |
|
||||
| Supported data types | `all` |
|
||||
| Programmatic fix | None |
|
||||
|
||||
Args:
|
||||
choices: The list of valid choices.
|
||||
"""
|
||||
|
||||
def __init__(self, choices: List[Any], on_fail: Optional[Callable] = None):
|
||||
super().__init__(
|
||||
on_fail=on_fail,
|
||||
choices=choices,
|
||||
)
|
||||
self._choices = choices
|
||||
|
||||
def validate(self, value: Any, metadata: Dict) -> ValidationResult:
|
||||
"""Validates that a value is within a range."""
|
||||
logger.debug(f"Validating {value} is in choices {self._choices}...")
|
||||
|
||||
if value not in self._choices:
|
||||
return FailResult(
|
||||
errorMessage=f"Value {value} is not in choices {self._choices}.",
|
||||
)
|
||||
|
||||
return PassResult()
|
||||
@@ -0,0 +1,88 @@
|
||||
import string
|
||||
from typing import Callable, Dict, List, Optional, Union
|
||||
|
||||
import rstr
|
||||
|
||||
from guardrails.logger import logger
|
||||
from guardrails.utils.casting_utils import to_int
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
|
||||
|
||||
@register_validator(name="length", data_type=["string", "list"])
|
||||
class ValidLength(Validator):
|
||||
"""Validates that the length of value is within the expected range.
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | --------------------------------- |
|
||||
| Name for `format` attribute | `length` |
|
||||
| Supported data types | `string`, `list`, `object` |
|
||||
| Programmatic fix | If shorter than the minimum, pad with empty last elements. If longer than the maximum, truncate. |
|
||||
|
||||
Args:
|
||||
min: The inclusive minimum length.
|
||||
max: The inclusive maximum length.
|
||||
""" # noqa
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
min: Optional[int] = None,
|
||||
max: Optional[int] = None,
|
||||
on_fail: Optional[Callable] = None,
|
||||
):
|
||||
super().__init__(
|
||||
on_fail=on_fail,
|
||||
min=min,
|
||||
max=max,
|
||||
)
|
||||
self._min = to_int(min)
|
||||
self._max = to_int(max)
|
||||
|
||||
def validate(self, value: Union[str, List], metadata: Dict) -> ValidationResult:
|
||||
"""Validates that the length of value is within the expected range."""
|
||||
logger.debug(
|
||||
f"Validating {value} is in length range {self._min} - {self._max}..."
|
||||
)
|
||||
|
||||
if self._min is not None and len(value) < self._min:
|
||||
logger.debug(f"Value {value} is less than {self._min}.")
|
||||
|
||||
# Repeat the last character to make the value the correct length.
|
||||
if isinstance(value, str):
|
||||
if not value:
|
||||
last_val = rstr.rstr(string.ascii_lowercase, 1)
|
||||
else:
|
||||
last_val = value[-1]
|
||||
corrected_value = value + last_val * (self._min - len(value))
|
||||
else:
|
||||
if not value:
|
||||
last_val = [rstr.rstr(string.ascii_lowercase, 1)]
|
||||
else:
|
||||
last_val = [value[-1]]
|
||||
# extend value by padding it out with last_val
|
||||
corrected_value = value.extend([last_val] * (self._min - len(value)))
|
||||
|
||||
return FailResult(
|
||||
errorMessage=f"Value has length less than {self._min}. "
|
||||
f"Please return a longer output, "
|
||||
f"that is shorter than {self._max} characters.",
|
||||
fixValue=corrected_value,
|
||||
)
|
||||
|
||||
if self._max is not None and len(value) > self._max:
|
||||
logger.debug(f"Value {value} is greater than {self._max}.")
|
||||
return FailResult(
|
||||
errorMessage=f"Value has length greater than {self._max}. "
|
||||
f"Please return a shorter output, "
|
||||
f"that is shorter than {self._max} characters.",
|
||||
fixValue=value[: self._max],
|
||||
)
|
||||
|
||||
return PassResult()
|
||||
@@ -0,0 +1,44 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from guardrails.logger import logger
|
||||
from guardrails.validator_base import (
|
||||
FailResult,
|
||||
PassResult,
|
||||
ValidationResult,
|
||||
Validator,
|
||||
register_validator,
|
||||
)
|
||||
|
||||
|
||||
@register_validator(name="valid-url", data_type=["string"])
|
||||
class ValidURL(Validator):
|
||||
"""Validates that a value is a valid URL.
|
||||
|
||||
**Key Properties**
|
||||
|
||||
| Property | Description |
|
||||
| ----------------------------- | --------------------------------- |
|
||||
| Name for `format` attribute | `valid-url` |
|
||||
| Supported data types | `string` |
|
||||
| Programmatic fix | None |
|
||||
"""
|
||||
|
||||
def validate(self, value: Any, metadata: Dict) -> ValidationResult:
|
||||
logger.debug(f"Validating {value} is a valid URL...")
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
# Check that the URL is valid
|
||||
try:
|
||||
result = urlparse(value)
|
||||
# Check that the URL has a scheme and network location
|
||||
if not result.scheme or not result.netloc:
|
||||
return FailResult(
|
||||
error_message=f"URL {value} is not valid.",
|
||||
)
|
||||
except ValueError:
|
||||
return FailResult(
|
||||
error_message=f"URL {value} is not valid.",
|
||||
)
|
||||
|
||||
return PassResult()
|
||||
Reference in New Issue
Block a user