참고소스 수정본

This commit is contained in:
LASTA_DEV01\lasta
2026-05-12 19:40:31 +09:00
parent 0f34a451fc
commit 2e9204243d
8708 changed files with 3259488 additions and 869 deletions

View File

@@ -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",
]

View File

@@ -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,
)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()