참고소스 수정본

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,21 @@
from guardrails.classes.rc import RC
from guardrails.classes.input_type import InputType
from guardrails.classes.output_type import OT
from guardrails.classes.validation.validation_result import (
ValidationResult,
PassResult,
FailResult,
ErrorSpan,
)
from guardrails.classes.validation_outcome import ValidationOutcome
__all__ = [
"RC",
"ErrorSpan",
"InputType",
"OT",
"ValidationResult",
"PassResult",
"FailResult",
"ValidationOutcome",
]

View File

@@ -0,0 +1,3 @@
from guardrails.classes.execution.guard_execution_options import GuardExecutionOptions
__all__ = ["GuardExecutionOptions"]

View File

@@ -0,0 +1,9 @@
from typing import Dict, List, Optional
from dataclasses import dataclass
@dataclass
class GuardExecutionOptions:
messages: Optional[List[Dict]] = None
reask_messages: Optional[List[Dict]] = None
num_reasks: Optional[int] = None

View File

@@ -0,0 +1,5 @@
from guardrails.classes.generic.arbitrary_model import ArbitraryModel
from guardrails.classes.generic.serializeable import Serializeable
from guardrails.classes.generic.stack import Stack
__all__ = ["ArbitraryModel", "Stack", "Serializeable"]

View File

@@ -0,0 +1,12 @@
from pydantic import BaseModel
class ArbitraryModel(BaseModel):
"""Empty Pydantic model with a config that allows arbitrary types and
aliases."""
model_config = {
"validate_by_alias": True,
"validate_by_name": True,
"arbitrary_types_allowed": True,
}

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from typing import Generic, TypeVar
from pydantic import BaseModel, Field, model_serializer
T = TypeVar("T")
class SerializeableAsyncIterable(BaseModel, Generic[T]):
content: list[T] = Field(default_factory=lambda: [])
_index: int = 0
@model_serializer(mode="plain")
def serialize_model(self) -> list[T]:
return self.content
async def __anext__(self) -> T:
if self._index >= len(self.content):
raise StopAsyncIteration
value = self.content[self._index]
self._index += 1
return value
def __aiter__(self) -> SerializeableAsyncIterable[T]:
self._index = 0
return self

View File

@@ -0,0 +1,21 @@
from datetime import datetime
from dataclasses import asdict, is_dataclass
from pydantic import BaseModel
from json import JSONEncoder
class DefaultJSONEncoder(JSONEncoder):
def default(self, o):
if hasattr(o, "to_dict"):
return o.to_dict()
elif isinstance(o, BaseModel):
return o.model_dump()
elif is_dataclass(o):
return asdict(o)
elif isinstance(o, set):
return list(o)
elif isinstance(o, datetime):
return o.isoformat()
elif hasattr(o, "__dict__"):
return o.__dict__
return super().default(o)

View File

@@ -0,0 +1,52 @@
import inspect
import json
import sys
from dataclasses import InitVar, asdict, dataclass, field, is_dataclass
from json import JSONEncoder
from typing import Any, Dict
from pydash.strings import snake_case
def get_annotations(obj):
if sys.version_info.minor >= 10 and hasattr(inspect, "get_annotations"):
return inspect.get_annotations(obj) # type: ignore
else:
return obj.__annotations__
class SerializeableJSONEncoder(JSONEncoder):
def default(self, o):
if is_dataclass(o):
return asdict(o)
return super().default(o)
encoder_kwargs = {}
if sys.version_info.minor >= 10:
encoder_kwargs["kw_only"] = True
encoder_kwargs["default"] = SerializeableJSONEncoder
@dataclass
class Serializeable:
encoder: InitVar[JSONEncoder] = field(**encoder_kwargs)
@classmethod
def from_dict(cls, data: Dict[str, Any]):
annotations = get_annotations(cls)
attributes = dict.keys(annotations)
snake_case_kwargs = {
snake_case(k): data.get(k) for k in data if snake_case(k) in attributes
}
snake_case_kwargs["encoder"] = snake_case_kwargs.get(
"encoder", SerializeableJSONEncoder
)
return cls(**snake_case_kwargs) # type: ignore
@property
def __dict__(self) -> Dict[str, Any]:
return asdict(self)
def to_json(self):
return json.dumps(self, cls=self.encoder) # type: ignore

View File

@@ -0,0 +1,114 @@
from typing import List, Optional, TypeVar
T = TypeVar("T")
class Stack(List[T]):
_max_length: Optional[int]
def __init__(self, *args, max_length: Optional[int] = None):
initial_entries = args
if max_length:
initial_entries = initial_entries[:max_length]
super().__init__(initial_entries)
self._max_length = max_length
def empty(self) -> bool:
"""Tests if this stack is empty."""
return len(self) == 0
def peek(self) -> Optional[T]:
"""Looks at the object at the top (last/most recently added) of this
stack without removing it from the stack."""
return self.at(-1)
def pop(self) -> Optional[T]:
"""Removes the object at the top of this stack and returns that object
as the value of this function."""
try:
value = super().pop()
return value
except IndexError:
pass
def push(self, item: T) -> None:
"""Pushes an item onto the top of this stack.
Proxy of List.append
Limits Stack Length to _max_length entries
"""
self.append(item)
if self._max_length:
del self[: -self._max_length]
def search(self, x: T) -> Optional[int]:
"""Returns the 0-based position of the last item whose value is equal
to x on this stack.
We deviate from the typical 1-based position used by Stack
classes (i.e. Java) because most python users (and developers in
general) are accustomed to 0-based indexing.
"""
copy = self.copy()
copy.reverse()
try:
found_index = copy.index(x)
return len(self) - found_index - 1
except ValueError:
pass
def at(self, index: int, default: Optional[T] = None) -> Optional[T]:
"""Returns the item located at the index.
If the index does not exist in the stack (Overflow or
Underflow), None is returned instead.
"""
try:
value = self[index]
return value
except IndexError:
return default
def copy(self) -> "Stack[T]":
"""Returns a copy of the current Stack."""
copy = super().copy()
return Stack(*copy)
@property
def first(self) -> Optional[T]:
"""Returns the first item of the stack without removing it.
Same as Stack.bottom.
"""
return self.at(0)
@property
def last(self) -> Optional[T]:
"""Returns the last item of the stack without removing it.
Same as Stack.top.
"""
return self.at(-1)
@property
def bottom(self) -> Optional[T]:
"""Returns the item on the bottom of the stack without removing it.
Same as Stack.first.
"""
return self.at(0)
@property
def top(self) -> Optional[T]:
"""Returns the item on the top of the stack without removing it.
Same as Stack.last.
"""
return self.at(-1)
@property
def length(self) -> int:
"""Returns the number of items in the Stack."""
return len(self)

View File

@@ -0,0 +1,7 @@
from guardrails.classes.history.call import Call
from guardrails.classes.history.call_inputs import CallInputs
from guardrails.classes.history.inputs import Inputs
from guardrails.classes.history.iteration import Iteration
from guardrails.classes.history.outputs import Outputs
__all__ = ["Call", "Iteration", "Inputs", "Outputs", "CallInputs"]

View File

@@ -0,0 +1,459 @@
from __future__ import annotations
from typing import Any, Dict, List, Optional, Union, Iterable
from builtins import id as object_id
from pydantic import Field, field_serializer, field_validator, computed_field
from rich.panel import Panel
from rich.pretty import pretty_repr
from rich.tree import Tree
from typing_extensions import deprecated
from guardrails_ai.types import Outcome, ValidationResult
from guardrails.actions.filter import Filter
from guardrails.actions.refrain import Refrain
from guardrails.actions.reask import merge_reask_output
from guardrails.classes.generic.stack import Stack
from guardrails.classes.history.call_inputs import CallInputs
from guardrails.classes.history.iteration import Iteration
from guardrails.classes.generic.arbitrary_model import ArbitraryModel
from guardrails.constants import error_status, fail_status, not_run_status, pass_status
from guardrails.prompt.messages import Messages
from guardrails.prompt import Prompt, Instructions
from guardrails.classes.validation.validator_logs import ValidatorLogs
from guardrails.actions.reask import (
ReAsk,
gather_reasks,
sub_reasks_with_fixed_values,
)
from guardrails.schema.parser import get_value_from_path
# We can't inherit from Iteration because python
# won't let you override a class attribute with a managed attribute
class Call(ArbitraryModel):
"""A Call represents a single execution of a Guard. One Call is created
each time the user invokes the `Guard.__call__`, `Guard.parse`, or
`Guard.validate` method.
Attributes:
iterations (Stack[Iteration]): A stack of iterations
for the initial validation round
and one for each reask that occurs during a Call.
inputs (CallInputs): The inputs as passed in to
`Guard.__call__`, `Guard.parse`, or `Guard.validate`
exception (Optional[Exception]): The exception that interrupted
the Guard execution.
"""
_id: str | None = None
iterations: Stack[Iteration] = Field(
description="A stack of iterations for each"
"step/reask that occurred during this call.",
default_factory=Stack,
)
inputs: CallInputs = Field(
description="The inputs as passed in to Guard.__call__ or Guard.parse",
default_factory=CallInputs,
)
exception: Optional[Exception] = Field(
description="The exception that interrupted the run.",
default=None,
)
@computed_field
@property
def id(self) -> str:
"""The unique identifier for this Call.
Can be used as an identifier for a specific execution of a
Guard.
"""
if not self._id:
self._id = str(object_id(self))
return self._id
@field_serializer("iterations")
def serialize_iterations(
self, iterations: Stack[Iteration] | None
) -> list[dict[str, Any]] | None:
if iterations is not None:
return [i.model_dump(exclude_none=True, by_alias=True) for i in iterations]
return []
@field_validator("iterations", mode="before")
@classmethod
def deserialize_iterations(cls, iterations: Any) -> Stack[Iteration] | None:
if iterations is not None and isinstance(iterations, Iterable):
_iterations = []
for i in iterations:
if isinstance(i, Iteration):
_iterations.append(i)
else:
iteration = Iteration.model_validate(i)
iteration._id = i.get("id") or iteration._id
_iterations.append(iteration)
return Stack(*_iterations)
return Stack()
@field_serializer("exception")
def serialize_exception(self, exception: Exception | None) -> str | None:
if exception:
return str(exception)
return None
@field_validator("exception", mode="before")
@classmethod
def deserialize_exception(cls, exception: Any) -> Exception | None:
if isinstance(exception, Exception):
return exception
if exception and isinstance(exception, str):
return Exception(exception)
return None
@property
def prompt_params(self) -> Optional[Dict]:
"""The prompt parameters as provided by the user when initializing or
calling the Guard."""
return self.inputs.prompt_params
@property
def messages(self) -> Optional[Union[Messages, list[dict[str, str]]]]:
"""The messages as provided by the user when initializing or calling
the Guard."""
return self.inputs.messages
@property
def compiled_messages(self) -> Optional[list[dict[str, str]]]:
"""The initial compiled messages that were passed to the LLM on the
first call."""
if self.iterations.empty():
return None
initial_inputs = self.iterations.first.inputs # type: ignore
messages = initial_inputs.messages
prompt_params = initial_inputs.prompt_params or {}
compiled_messages = []
if messages is None:
return None
for message in messages:
content = message["content"].format(**prompt_params)
if isinstance(content, (Prompt, Instructions)):
content = content._source
compiled_messages.append(
{
"role": message["role"],
"content": content,
}
)
return compiled_messages
@property
def reask_messages(self) -> Stack[Messages]:
"""The compiled messages used during reasks.
Does not include the initial messages.
"""
if self.iterations.length > 0:
reasks = self.iterations.copy()
initial_messages = reasks.first
reasks.remove(initial_messages) # type: ignore
initial_inputs = self.iterations.first.inputs # type: ignore
prompt_params = initial_inputs.prompt_params or {}
compiled_reasks = []
for reask in reasks:
messages = reask.inputs.messages
if messages is None:
compiled_reasks.append(None)
else:
compiled_messages = []
for message in messages:
content = message["content"].format(**prompt_params)
if isinstance(content, (Prompt, Instructions)):
content = content._source
compiled_messages.append(
{
"role": message["role"],
"content": content,
}
)
compiled_reasks.append(compiled_messages)
return Stack(*compiled_reasks)
return Stack()
@property
def logs(self) -> Stack[str]:
"""Returns all logs from all iterations as a stack."""
all_logs = []
for i in self.iterations:
all_logs.extend(i.logs)
return Stack(*all_logs)
@property
def tokens_consumed(self) -> Optional[int]:
"""Returns the total number of tokens consumed during all iterations
with this call."""
iteration_tokens = [
i.tokens_consumed for i in self.iterations if i.tokens_consumed is not None
]
if len(iteration_tokens) > 0:
return sum(iteration_tokens)
return None
@property
def prompt_tokens_consumed(self) -> Optional[int]:
"""Returns the total number of prompt tokens consumed during all
iterations with this call."""
iteration_tokens = [
i.prompt_tokens_consumed
for i in self.iterations
if i.prompt_tokens_consumed is not None
]
if len(iteration_tokens) > 0:
return sum(iteration_tokens)
return None
@property
def completion_tokens_consumed(self) -> Optional[int]:
"""Returns the total number of completion tokens consumed during all
iterations with this call."""
iteration_tokens = [
i.completion_tokens_consumed
for i in self.iterations
if i.completion_tokens_consumed is not None
]
if len(iteration_tokens) > 0:
return sum(iteration_tokens)
return None
@property
def raw_outputs(self) -> Stack[str]:
"""The exact outputs from all LLM calls."""
return Stack(
*[
i.outputs.llm_response_info.output
if i.outputs.llm_response_info is not None
else None
for i in self.iterations
]
)
@property
def parsed_outputs(self) -> Stack[Union[str, List, Dict]]:
"""The outputs from the LLM after undergoing parsing but before
validation."""
return Stack(*[i.outputs.parsed_output for i in self.iterations])
@property
def validation_response(self) -> Optional[Union[str, List, Dict, ReAsk]]:
"""The aggregated responses from the validation process across all
iterations within the current call.
This value could contain ReAsks.
"""
number_of_iterations = self.iterations.length
if number_of_iterations == 0:
return None
# Don't try to merge if
# 1. We plan to perform full schema reasks
# 2. There's nothing to merge
# 3. The output is a top level ReAsk (i.e. SkeletonReAsk or NonParseableReask)
# 4. The output is a string
if (
self.inputs.full_schema_reask
or number_of_iterations < 2
or isinstance(
self.iterations.last.validation_response, # type: ignore
ReAsk, # type: ignore
)
or isinstance(self.iterations.last.validation_response, str) # type: ignore
):
return self.iterations.last.validation_response # type: ignore
current_index = 1
# We've already established that there are iterations,
# hence the type ignores
merged_validation_responses = (
self.iterations.first.validation_response # type: ignore
)
while current_index < number_of_iterations:
current_validation_output = self.iterations.at(
current_index
).validation_response # type: ignore
merged_validation_responses = merge_reask_output(
merged_validation_responses, current_validation_output
)
current_index = current_index + 1
return merged_validation_responses
@property
def fixed_output(self) -> Optional[Union[str, List, Dict]]:
"""The cumulative output from the validation process across all current
iterations with any automatic fixes applied.
Could still contain ReAsks if a fix was not available.
"""
return sub_reasks_with_fixed_values(self.validation_response)
@property
def guarded_output(self) -> Optional[Union[str, List, Dict]]:
"""The complete validated output after all stages of validation are
completed.
This property contains the aggregate validated output after all
validation stages have been completed. Some values in the
validated output may be "fixed" values that were corrected
during validation.
This will only have a value if the Guard is in a passing state
OR if the action is no-op.
"""
if self.status == pass_status:
return self.fixed_output
last_iteration = self.iterations.last
if (
not self.status == pass_status
and last_iteration
and last_iteration.failed_validations
):
# check that all failed validations are noop or none
all_noop = True
for failed_validation in last_iteration.failed_validations:
if (
failed_validation.value_after_validation
is not failed_validation.value_before_validation
):
all_noop = False
break
if all_noop:
return last_iteration.guarded_output
@property
def reasks(self) -> Stack[ReAsk]:
"""Reasks generated during validation that could not be automatically
fixed.
These would be incorporated into the prompt for the next LLM
call if additional reasks were granted.
"""
reasks, _ = gather_reasks(self.fixed_output)
return Stack(*reasks)
@property
def validator_logs(self) -> Stack[ValidatorLogs]:
"""The results of each individual validation performed on the LLM
responses during all iterations."""
all_validator_logs = Stack()
for i in self.iterations:
all_validator_logs.extend(i.validator_logs)
return all_validator_logs
@property
def error(self) -> Optional[str]:
"""The error message from any exception that raised and interrupted the
run."""
if self.exception:
return str(self.exception)
elif self.iterations.empty():
return None
return self.iterations.last.error # type: ignore
@property
def failed_validations(self) -> Stack[ValidatorLogs]:
"""The validator logs for any validations that failed during the
entirety of the run."""
return Stack(
*[
log
for log in self.validator_logs
if log.validation_result is not None
and isinstance(log.validation_result, ValidationResult)
and log.validation_result.outcome == Outcome.FAIL
]
)
def _has_unresolved_failures(self) -> bool:
# Check for unresolved ReAsks
if len(self.reasks) > 0:
return True
# Check for scenario where no specified on-fail's produced an unfixed ReAsk,
# but valdiation still failed (i.e. Refrain or NoOp).
output = self.fixed_output
for failure in self.failed_validations:
value = get_value_from_path(output, failure.property_path)
if (
# NOTE: this means on_fail="fix" was applied
# to a Validator without a programmatic fix.
(value is None and failure.value_before_validation is not None)
or value == failure.value_before_validation
or isinstance(failure.value_after_validation, Refrain)
or isinstance(failure.value_after_validation, Filter)
):
return True
# No ReAsks and no unresolved failed validations
return False
@property
def status(self) -> str:
"""Returns the cumulative status of the run based on the validity of
the final merged output."""
if self.iterations.empty():
return not_run_status
elif self.error:
return error_status
elif self._has_unresolved_failures():
return fail_status
return pass_status
@property
def tree(self) -> Tree:
"""Returns the tree."""
tree = Tree("Logs")
for i, iteration in enumerate(self.iterations):
tree.add(Panel(iteration.rich_group, title=f"Step {i}"))
# Replace the last Validated Output panel if we applied fixes
if self.failed_validations.length > 0 and self.status == pass_status:
previous_panels = tree.children[ # type: ignore
-1
].label.renderable._renderables[ # type: ignore
:-1
]
validated_outcome_panel = Panel(
pretty_repr(self.guarded_output),
title="Validated Output",
style="on #F0FFF0",
)
tree.children[-1].label.renderable._renderables = previous_panels + ( # type: ignore
validated_outcome_panel,
)
return tree
def __str__(self) -> str:
return pretty_repr(self)
@deprecated("Use Call.model_dump() instead.")
def to_interface(self) -> dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@deprecated("Use Call.model_dump() instead.")
def to_dict(self) -> dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@classmethod
@deprecated("Use Call.model_validate() instead.")
def from_interface(cls, i_call: Any) -> "Call":
return cls.model_validate(i_call)
# TODO: Necessary to GET /guards/{guard_name}/history/{call_id}
@classmethod
@deprecated("Use Call.model_validate() instead.")
def from_dict(cls, obj: Any) -> "Call":
return cls.model_validate(obj)

View File

@@ -0,0 +1,128 @@
from __future__ import annotations
from typing import Any, Awaitable, Callable, Dict, Iterable, List, Optional
from typing_extensions import deprecated
from pydantic import Field, field_serializer, field_validator
from guardrails.classes.history.inputs import Inputs
from guardrails.classes.generic.arbitrary_model import ArbitraryModel
from guardrails.prompt.base_prompt import BasePrompt
class CallInputs(Inputs, ArbitraryModel):
"""CallInputs represent the input data that is passed into the Guard from
the user.
Inherits from Inputs with the below overrides and additional
attributes.
"""
llm_api: Optional[Callable[[Any], Awaitable[Any]]] = Field(
description="The LLM function provided by the user"
"during Guard.__call__ or Guard.parse.",
default=None,
alias="llmApi",
)
llm_output: Optional[str] = Field(
default=None,
description="The string output from an external LLM call provided by the user"
" via Guard.parse.",
alias="llmOutput",
)
messages: Optional[list[dict[str, str]]] = Field(
description="The messages as provided by the user.", default=None
)
prompt_params: Optional[Dict[str, Any]] = Field(
default=None,
description="Parameters to be formatted into the messages.",
alias="promptParams",
)
num_reasks: Optional[int] = Field(
default=None,
description="The total number of times the LLM can be called to correct output"
" excluding the initial call.",
alias="numReasks",
)
metadata: Optional[Dict[str, Any]] = Field(
default=None,
description="Additional data to be used by Validators during execution time.",
)
full_schema_reask: Optional[bool] = Field(
default=None,
description="Whether to perform reasks for the entire schema rather than for"
" individual fields.",
alias="fullSchemaReask",
)
stream: Optional[bool] = Field(
default=None, description="Whether to use streaming."
)
args: List[Any] = Field(
description="Additional arguments for the LLM as provided by the user.",
default_factory=list,
)
kwargs: Dict[str, Any] = Field(
description="Additional keyword-arguments for the LLM as provided by the user.",
default_factory=dict,
)
@field_serializer("llm_api")
def serialize_llm_api(
self, llm_api: Callable[[Any], Awaitable[Any]] | None
) -> str | None:
if llm_api:
return str(llm_api)
return None
@field_validator("llm_api", mode="before")
@classmethod
def deserialize_llm_api(
cls, llm_api: Any
) -> Callable[[Any], Awaitable[Any]] | None:
if callable(llm_api):
return llm_api # type: ignore
# Note: We can potentially identify the correct
# PrompCallable Class and reconstruct it,
# but the previous implementation always just returned None.
return None
@field_validator("messages", mode="before")
@classmethod
def deserialize_messages(cls, messages: Any) -> list[dict[str, str]] | None:
if messages is not None and isinstance(messages, Iterable):
serialized_messages = []
for msg in messages:
ser_msg = {**msg}
content = ser_msg.get("content")
if content:
ser_msg["content"] = (
content.source if isinstance(content, BasePrompt) else content
)
serialized_messages.append(ser_msg)
return serialized_messages
return None
@field_serializer("kwargs")
def serialize_kwargs(self, kwargs: dict[str, Any]) -> dict[str, Any]:
redacted_kwargs = {}
for k, v in kwargs.items():
if ("key" in k.lower() or "token" in k.lower()) and isinstance(v, str):
redaction_length = len(v) - 4
stars = "*" * redaction_length
redacted_kwargs[k] = f"{stars}{v[-4:]}"
else:
redacted_kwargs[k] = v
return redacted_kwargs
@deprecated("Use CallInputs.model_dump() instead.")
def to_dict(self) -> Dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@classmethod
@deprecated("Use CallInputs.model_validate() instead.")
def from_interface(cls, i_call_inputs: Any) -> "CallInputs":
return cls.model_validate(i_call_inputs)
@classmethod
@deprecated("Use CallInputs.model_validate() instead.")
def from_dict(cls, obj: Any):
return cls.model_validate(obj)

View File

@@ -0,0 +1,106 @@
from __future__ import annotations
from typing_extensions import deprecated
from typing import Any, Dict, List, Optional, Union
from pydantic import Field, field_serializer, field_validator
from guardrails.classes.generic.arbitrary_model import ArbitraryModel
from guardrails.classes.llm.prompt_callable import PromptCallableBase
from guardrails.prompt.prompt import Prompt
from guardrails.prompt.messages import Messages
from guardrails.prompt.instructions import Instructions
class Inputs(ArbitraryModel):
"""Inputs represent the input data that is passed into the validation
loop."""
llm_api: Optional[PromptCallableBase] = Field(
description="The constructed class for calling the LLM.", default=None
)
llm_output: Optional[str] = Field(
description="The string output from an external LLM call"
"provided by the user via Guard.parse.",
default=None,
)
messages: Optional[
Union[List[Dict[str, Union[str, Prompt, Instructions]]], Messages]
] = Field(
description="The message history provided by the user for chat model calls.",
default=None,
)
prompt_params: Optional[Dict] = Field(
description="The parameters provided by the user"
"that will be formatted into the final LLM prompt.",
default=None,
)
num_reasks: Optional[int] = Field(
description="The total number of reasks allowed; user provided or defaulted.",
default=None,
)
metadata: Optional[Dict[str, Any]] = Field(
description="The metadata provided by the user to be used during validation.",
default=None,
)
full_schema_reask: Optional[bool] = Field(
description="Whether to perform reasks across the entire schema"
"or at the field level.",
default=None,
)
stream: Optional[bool] = Field(
description="Whether to use streaming.",
default=False,
)
@field_serializer("llm_api")
def serialize_llm_api(self, llm_api: PromptCallableBase | None) -> str | None:
if llm_api:
return str(llm_api)
return None
@field_validator("llm_api", mode="before")
@classmethod
def deserialize_llm_api(cls, llm_api: Any) -> PromptCallableBase | None:
if isinstance(llm_api, PromptCallableBase):
return llm_api
# Note: We can potentially identify the correct
# PrompCallable Class and reconstruct it,
# but the previous implementation always just returned None.
return None
@field_serializer("messages")
def serialize_messages(
self, messages: list[dict[str, str | Prompt | Instructions]] | Messages | None
) -> list[dict[str, Any]] | None:
# Legacy serialization logic from previous to_interface implementation
# TODO: Just make Prompt, Instructions, and Messages pydantic models
if messages:
serialized_messages = []
for msg in messages:
ser_msg = {**msg}
content = ser_msg.get("content")
if content:
ser_msg["content"] = (
content.source if isinstance(content, Prompt) else content
)
serialized_messages.append(ser_msg)
return serialized_messages
return None
@deprecated("Use Inputs.model_dump() instead.")
def to_interface(self) -> dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@deprecated("Use Inputs.model_dump() instead.")
def to_dict(self) -> dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@classmethod
@deprecated("Use Inputs.model_validate() instead.")
def from_interface(cls, i_inputs: Any) -> "Inputs":
return cls.model_validate(i_inputs)
@classmethod
@deprecated("Use Inputs.model_validate() instead.")
def from_dict(cls, obj: Any) -> "Inputs":
return cls.model_validate(obj)

View File

@@ -0,0 +1,234 @@
from __future__ import annotations
from typing_extensions import deprecated
from typing import Any, Dict, List, Optional, Sequence, Union
from builtins import id as object_id
from pydantic import Field, computed_field
from rich.console import Group
from rich.panel import Panel
from rich.pretty import pretty_repr
from rich.table import Table
from guardrails.classes.generic.stack import Stack
from guardrails.classes.history.inputs import Inputs
from guardrails.classes.history.outputs import Outputs
from guardrails.classes.generic.arbitrary_model import ArbitraryModel
from guardrails.logger import get_scope_handler
from guardrails.prompt import Prompt, Instructions
from guardrails.classes.validation.validator_logs import ValidatorLogs
from guardrails.actions.reask import ReAsk
from guardrails_ai.types import ErrorSpan
class Iteration(ArbitraryModel):
"""An Iteration represents a single iteration of the validation loop
including a single call to the LLM if applicable."""
_id: str | None = None
index: int = Field(
description="The zero-based index of this iteration within the current Call.",
default=0,
)
call_id: str = Field(
description="The unique identifier for the Call that this"
" iteration is a part of.",
alias="callId",
default="0",
)
inputs: Inputs = Field(
description="The inputs for the iteration/step.", default_factory=Inputs
)
# We might just spread these properties instead of containering them
outputs: Outputs = Field(
description="The outputs from the iteration/step.", default_factory=Outputs
)
@computed_field
@property
def id(self) -> str:
"""The unique identifier for this Call.
Can be used as an identifier for a specific execution of a
Guard.
"""
if not self._id:
self._id = str(object_id(self))
return self._id
@property
def logs(self) -> Stack[str]:
"""Returns the logs from this iteration as a stack."""
scope = str(id(self))
scope_handler = get_scope_handler()
scoped_logs = scope_handler.get_logs(scope)
return Stack(*[log.getMessage() for log in scoped_logs])
@property
def tokens_consumed(self) -> Optional[int]:
"""Returns the total number of tokens consumed during this
iteration."""
input_tokens = self.prompt_tokens_consumed
output_tokens = self.completion_tokens_consumed
if input_tokens is not None or output_tokens is not None:
return (input_tokens or 0) + (output_tokens or 0)
@property
def prompt_tokens_consumed(self) -> Optional[int]:
"""Returns the number of prompt/input tokens consumed during this
iteration."""
response = self.outputs.llm_response_info
if response is not None:
return response.prompt_token_count
@property
def completion_tokens_consumed(self) -> Optional[int]:
"""Returns the number of completion/output tokens consumed during this
iteration."""
response = self.outputs.llm_response_info
if response is not None:
return response.response_token_count
@property
def raw_output(self) -> Optional[str]:
"""The exact output from the LLM."""
response = self.outputs.llm_response_info
if response is not None and response.output:
return response.output
elif self.outputs.raw_output is not None:
return self.outputs.raw_output
@property
def parsed_output(self) -> Optional[Union[str, List, Dict]]:
"""The output from the LLM after undergoing parsing but before
validation."""
return self.outputs.parsed_output
@property
def validation_response(self) -> Optional[Union[ReAsk, str, List, Dict]]:
"""The response from a single stage of validation.
Validation response is the output of a single stage of validation
and could be a combination of valid output and reasks.
Note that a Guard may run validation multiple times if reasks occur.
To access the final output after all steps of validation are completed,
check out `Call.guarded_output`."
"""
return self.outputs.validation_response
@property
def guarded_output(self) -> Optional[Union[str, List, Dict]]:
"""Any valid values after undergoing validation.
Some values in the validated output may be "fixed" values that
were corrected during validation. This property may be a partial
structure if field level reasks occur.
"""
return self.outputs.guarded_output
@property
def reasks(self) -> Sequence[ReAsk]:
"""Reasks generated during validation.
These would be incorporated into the prompt or the next LLM
call.
"""
return self.outputs.reasks
@property
def validator_logs(self) -> List[ValidatorLogs]:
"""The results of each individual validation performed on the LLM
response during this iteration."""
if self.inputs.stream:
filtered_logs = [
log
for log in self.outputs.validator_logs
if log.validation_result and log.validation_result.validated_chunk
]
return filtered_logs
return self.outputs.validator_logs
@property
def error(self) -> Optional[str]:
"""The error message from any exception that raised and interrupted
this iteration."""
return self.outputs.error
@property
def exception(self) -> Optional[Exception]:
"""The exception that interrupted this iteration."""
return self.outputs.exception
@property
def failed_validations(self) -> List[ValidatorLogs]:
"""The validator logs for any validations that failed during this
iteration."""
return self.outputs.failed_validations
@property
def error_spans_in_output(self) -> List[ErrorSpan]:
"""The error spans from the LLM response.
These indices are relative to the complete LLM output.
"""
return self.outputs.error_spans_in_output
@property
def status(self) -> str:
"""Representation of the end state of this iteration.
OneOf: pass, fail, error, not run
"""
return self.outputs.status
@property
def rich_group(self) -> Group:
def create_messages_table(
messages: Optional[List[Dict[str, Union[str, Prompt, Instructions]]]],
) -> Union[str, Table]:
if messages is None:
return "No messages."
table = Table(show_lines=True)
table.add_column("Role", justify="right", no_wrap=True)
table.add_column("Content")
for msg in messages:
if hasattr(msg["content"], "source"):
table.add_row(str(msg["role"]), msg["content"].source) # type: ignore
else:
table.add_row(str(msg["role"]), msg["content"]) # type: ignore
return table
table = create_messages_table(self.inputs.messages) # type: ignore
return Group(
Panel(table, title="Messages", style="on #E7DFEB"),
Panel(self.raw_output or "", title="Raw LLM Output", style="on #F5F5DC"),
Panel(
self.validation_response
if isinstance(self.validation_response, str)
else pretty_repr(self.validation_response),
title="Validated Output",
style="on #F0FFF0",
),
)
def __str__(self) -> str:
return pretty_repr(self)
@deprecated("Use Iteration.model_dump() instead.")
def to_interface(self) -> dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@deprecated("Use Iteration.model_dump() instead.")
def to_dict(self) -> dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@classmethod
@deprecated("Use Iteration.model_validate() instead.")
def from_interface(cls, i_iteration: Any) -> "Iteration":
return cls.model_validate(i_iteration)
@classmethod
@deprecated("Use Iteration.model_validate() instead.")
def from_dict(cls, obj: Any) -> "Iteration":
return cls.model_validate(obj)

View File

@@ -0,0 +1,193 @@
from __future__ import annotations
from typing import Any, Dict, List, Optional, Union
from typing_extensions import deprecated
from pydantic import Field, field_serializer, field_validator, ValidationError
from guardrails_ai.types import FailResult, ValidationResult, ErrorSpan, Outcome
from guardrails.constants import error_status, fail_status, not_run_status, pass_status
from guardrails.classes.llm.llm_response import LLMResponse
from guardrails.classes.generic.arbitrary_model import ArbitraryModel
from guardrails.classes.validation.validator_logs import ValidatorLogs
from guardrails.actions.reask import to_reask, ReAsk
class Outputs(ArbitraryModel):
"""Outputs represent the data that is output from the validation loop."""
llm_response_info: Optional[LLMResponse] = Field(
description="Information from the LLM response.", default=None
)
raw_output: Optional[str] = Field(
description="The exact output from the LLM.", default=None
)
parsed_output: Optional[Union[str, List, Dict]] = Field(
description="The output parsed from the LLM response"
"as it was passed into validation.",
default=None,
)
validation_response: Optional[Union[str, ReAsk, List, Dict]] = Field(
description="The response from the validation process.", default=None
)
guarded_output: Optional[Union[str, List, Dict]] = Field(
description="""Any valid values after undergoing validation.
Some values may be "fixed" values that were corrected during validation.
This property may be a partial structure if field level reasks occur.""",
default=None,
)
reasks: List[ReAsk] = Field(
description="Information from the validation process"
"used to construct a ReAsk to the LLM on validation failure.",
default_factory=list,
)
# TODO: Rename this;
validator_logs: List[ValidatorLogs] = Field(
description="The results of each individual validation.", default_factory=list
)
error: Optional[str] = Field(
description="The error message from any exception"
"that raised and interrupted the process.",
default=None,
)
exception: Optional[Exception] = Field(
description="The exception that interrupted the process.", default=None
)
@field_validator("validation_response", mode="before")
@classmethod
def deserialize_validation_response(
cls, validation_response: Any | None
) -> str | ReAsk | List | Dict | None:
if isinstance(validation_response, ReAsk):
return validation_response
if validation_response and isinstance(validation_response, dict):
try:
return to_reask(validation_response)
except ValidationError:
return validation_response
return validation_response
@field_validator("reasks", mode="before")
@classmethod
def deserialize_reasks(cls, reasks: Any) -> List[ReAsk]:
if reasks and isinstance(reasks, list):
return [to_reask(r) if not isinstance(r, ReAsk) else r for r in reasks]
return []
@field_serializer("exception")
def serialize_exception(self, exception: Exception | None) -> str | None:
if exception:
return str(exception)
return None
@field_validator("exception", mode="before")
@classmethod
def deserialize_exception(cls, exception: Any) -> Exception | None:
if isinstance(exception, Exception):
return exception
if exception and isinstance(exception, str):
return Exception(exception)
return None
def _all_empty(self) -> bool:
return (
self.llm_response_info is None
and self.parsed_output is None
and self.validation_response is None
and self.guarded_output is None
and len(self.reasks) == 0
and len(self.validator_logs) == 0
and self.error is None
)
@property
def failed_validations(self) -> List[ValidatorLogs]:
"""Returns the validator logs for any validation that failed."""
return list(
[
log
for log in self.validator_logs
if log.validation_result is not None
and isinstance(log.validation_result, ValidationResult)
and log.validation_result.outcome == Outcome.FAIL
]
)
@property
def error_spans_in_output(self) -> List[ErrorSpan]:
"""The error spans from the LLM response.
These indices are relative to the complete LLM output.
"""
# map of total length to validator
total_len_by_validator = {}
spans_in_output = []
for log in self.validator_logs:
validator_name = log.validator_name
if total_len_by_validator.get(validator_name) is None:
total_len_by_validator[validator_name] = 0
result = log.validation_result
if isinstance(result, FailResult):
if result.error_spans is not None:
for error_span in result.error_spans:
spans_in_output.append(
ErrorSpan(
start=error_span.start
+ total_len_by_validator[validator_name],
end=error_span.end
+ total_len_by_validator[validator_name],
reason=error_span.reason,
)
)
if isinstance(result, ValidationResult):
if result and result.validated_chunk is not None:
total_len_by_validator[validator_name] += len(
result.validated_chunk
)
return spans_in_output
@property
def status(self) -> str:
"""Representation of the end state of the validation run.
OneOf: pass, fail, error, not run
"""
all_fail_results: List[FailResult] = []
for reask in self.reasks:
all_fail_results.extend(reask.fail_results or [])
all_reasks_have_fixes = all(
list(fail.fix_value is not None for fail in all_fail_results)
)
if self._all_empty() is True:
return not_run_status
elif self.error:
return error_status
elif not all_reasks_have_fixes:
return fail_status
elif self.guarded_output is None and isinstance(
self.validation_response, ReAsk
):
return fail_status
return pass_status
@deprecated("Use Outputs.model_dump() instead.")
def to_interface(self) -> dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@deprecated("Use Outputs.model_dump() instead.")
def to_dict(self) -> Dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@classmethod
@deprecated("Use Outputs.model_validate() instead.")
def from_interface(cls, i_outputs: Any) -> "Outputs":
return cls.model_validate(i_outputs)
@classmethod
@deprecated("Use Outputs.model_validate() instead.")
def from_dict(cls, obj: Any) -> "Outputs":
return cls.model_validate(obj)

View File

@@ -0,0 +1,5 @@
from typing import TypeVar
from langchain_core.messages import BaseMessage
InputType = TypeVar("InputType", str, BaseMessage)

View File

@@ -0,0 +1,142 @@
import asyncio
import warnings
from itertools import tee
from typing import Any, Dict, Iterator, Optional, AsyncIterator, Iterable, Tuple
from typing_extensions import deprecated
from pydantic import Field, field_serializer, field_validator
from guardrails.classes.generic.arbitrary_model import ArbitraryModel
from guardrails.classes.generic.async_iterable import SerializeableAsyncIterable
warnings.filterwarnings(
"ignore",
category=RuntimeWarning,
message="coroutine 'serialize_aiter' was never awaited",
)
# TODO: Move this somewhere that makes sense
def async_to_sync(awaitable):
loop = asyncio.get_event_loop()
return loop.run_until_complete(awaitable)
async def serialize_aiter(
async_iter: AsyncIterator,
) -> Tuple[Optional[list[str]], AsyncIterator]:
iter_output: list[str] = []
async for so in async_iter:
iter_output.append(str(so))
return iter_output, SerializeableAsyncIterable[str](content=iter_output)
# TODO: We might be able to delete this
class LLMResponse(ArbitraryModel):
"""Standard information collection from LLM responses to feed the
validation loop."""
# Pydantic Config
model_config = {
"validate_by_alias": True,
"validate_by_name": True,
"arbitrary_types_allowed": True,
}
prompt_token_count: Optional[int] = Field(
default=None,
alias="promptTokenCount",
description="The number of tokens in the prompt.",
)
response_token_count: Optional[int] = Field(
default=None,
alias="responseTokenCount",
description="The number of tokens in the response.",
)
output: str = Field(default="", description="The output from the LLM.")
stream_output: Optional[Iterator] = Field(
default=None,
alias="streamOutput",
description="A stream of output from the LLM.",
)
async_stream_output: Optional[AsyncIterator] = Field(
default=None,
alias="asyncStreamOutput",
description="An async stream of output from the LLM.",
)
@field_serializer("stream_output")
def serialize_stream_output(
self, stream_output: Iterator | None
) -> list[str] | None:
if stream_output:
copy_1, copy_2 = tee(stream_output)
self.stream_output = copy_1
ser_stream_output = [str(so) for so in copy_2]
return ser_stream_output
return None
@field_validator("stream_output", mode="before")
@classmethod
def deserialize_stream_output(cls, stream_output: Any | None) -> Iterator | None:
if isinstance(stream_output, Iterator):
return stream_output
if stream_output:
try:
return iter(stream_output)
except TypeError:
return None
return None
@field_serializer("async_stream_output")
def serialize_async_stream_output(
self, async_stream_output: AsyncIterator | None
) -> list[str] | None:
# Legacy serialization logic from previous to_interface implementation
# We probably need a wrapper class for these.
if async_stream_output and not hasattr(async_stream_output, "__aiter__"):
_async_stream_output = []
awaited_stream_output = []
for so in self.async_stream_output: # type: ignore - we just established it isn't None
_async_stream_output.append(so)
awaited_stream_output.append(str(async_to_sync(so)))
self.async_stream_output = aiter(_async_stream_output) # type: ignore # noqa: F821
return None
@field_validator("async_stream_output", mode="before")
@classmethod
def deserialize_async_stream_output(
cls, async_stream_output: Any | None
) -> AsyncIterator | None:
if isinstance(async_stream_output, AsyncIterator):
return async_stream_output
if async_stream_output and isinstance(async_stream_output, Iterable):
async def async_iter():
for aso in async_stream_output:
yield aso
return async_iter()
return None
@deprecated("Use LLMResponse.model_dump() instead.")
def to_interface(self) -> dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@deprecated("Use LLMResponse.model_dump() instead.")
def to_dict(self) -> Dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@classmethod
@deprecated("Use LLMResponse.model_validate() instead.")
def from_interface(cls, i_llm_response: Any) -> "LLMResponse":
return cls.model_validate(i_llm_response)
@classmethod
@deprecated("Use LLMResponse.model_validate() instead.")
def from_dict(cls, obj: Any) -> "LLMResponse":
return cls.model_validate(obj)

View File

@@ -0,0 +1,45 @@
from guardrails.classes.llm.llm_response import LLMResponse
CALLABLE_FAILURE_SUFFIX = """Make sure that `fn` can be called as a function
that accepts a prompt string, **kwargs, and returns a string.
If you're using a custom LLM callable, please see docs
here: https://go.guardrailsai.com/B1igEy3""" # noqa
class PromptCallableException(Exception):
pass
class PromptCallableBase:
"""A wrapper around a callable that takes in a prompt.
Catches exceptions to let the user know clearly if the callable
failed, and how to fix it.
"""
supports_base_model = False
def __init__(self, *args, **kwargs):
self.init_args = args
self.init_kwargs = kwargs
def _invoke_llm(self, *args, **kwargs) -> LLMResponse:
raise NotImplementedError
def __call__(self, *args, **kwargs) -> LLMResponse:
try:
result = self._invoke_llm(
*self.init_args, *args, **self.init_kwargs, **kwargs
)
except Exception as e:
raise PromptCallableException(
"The callable `fn` passed to `Guard(fn, ...)` failed"
f" with the following error: `{e}`. {CALLABLE_FAILURE_SUFFIX}"
)
if not isinstance(result, LLMResponse):
raise PromptCallableException(
"The callable `fn` passed to `Guard(fn, ...)` returned"
f" a non-string value: {result}. {CALLABLE_FAILURE_SUFFIX}"
)
return result

View File

@@ -0,0 +1,63 @@
# TODO: Move this file to guardrails.types
from enum import Enum
from typing import Any, Dict, List, Optional, TypeVar, Union
from guardrails.types.simple import SimpleTypes
OT = TypeVar("OT", str, List, Dict)
# TODO: Move this to types.py
# It's only here for historical reasons
class OutputTypes(str, Enum):
STRING = "str"
LIST = "list"
DICT = "dict"
@staticmethod
def get(key: Optional[Union[str, "OutputTypes"]], default=None):
try:
if not key:
return default
if isinstance(key, OutputTypes):
return key
return OutputTypes[key]
except Exception:
return default
@classmethod
def __from_json_schema__(cls, json_schema: Dict[str, Any]) -> "OutputTypes":
if not json_schema:
return cls("str")
schema_type = json_schema.get("type")
if schema_type == SimpleTypes.STRING:
return cls("str")
elif schema_type == SimpleTypes.OBJECT:
return cls("dict")
elif schema_type == SimpleTypes.ARRAY:
return cls("list")
all_of = json_schema.get("allOf")
if all_of:
return cls("dict")
one_of: List[Dict[str, Any]] = [
s
for s in json_schema.get("oneOf", [])
if isinstance(s, dict) and "type" in s
]
if one_of:
first_sub_schema = one_of[0]
return cls.__from_json_schema__(first_sub_schema)
any_of: List[Dict[str, Any]] = [
s
for s in json_schema.get("anyOf", [])
if isinstance(s, dict) and "type" in s
]
if any_of:
first_sub_schema = any_of[0]
return cls.__from_json_schema__(first_sub_schema)
# Fallback to string
return cls("str")

View File

@@ -0,0 +1,80 @@
import logging
import os
from dataclasses import dataclass
from os.path import expanduser
from typing import Optional
from guardrails.classes.generic.serializeable import Serializeable
from guardrails.utils.casting_utils import to_bool
BOOL_CONFIGS = set(["no_metrics", "enable_metrics", "use_remote_inferencing"])
@dataclass
class RC(Serializeable):
id: Optional[str] = None
token: Optional[str] = None
enable_metrics: Optional[bool] = True
use_remote_inferencing: Optional[bool] = True
@staticmethod
def exists() -> bool:
home = expanduser("~")
guardrails_rc = os.path.join(home, ".guardrailsrc")
return os.path.exists(guardrails_rc)
@classmethod
def load(cls, logger: Optional[logging.Logger] = None) -> "RC":
try:
if not logger:
logger = logging.getLogger()
home = expanduser("~")
guardrails_rc = os.path.join(home, ".guardrailsrc")
with open(guardrails_rc, encoding="utf-8") as rc_file:
lines = rc_file.readlines()
filtered_lines = list(filter(lambda l: l.strip(), lines))
config = {}
for line in filtered_lines:
line_content = line.split("=", 1)
if len(line_content) != 2:
logger.warning(
"""
Invalid line found in .guardrailsrc file!
All lines in this file should follow the format: key=value
Ignoring line contents...
"""
)
logger.debug(f".guardrailsrc file location: {guardrails_rc}")
else:
key, value = line_content
key = key.strip()
value = value.strip()
# Strip surrounding matching quotes so that
# e.g. token="" is treated as an empty string
# rather than the literal two-character value '""'.
if (
len(value) >= 2
and value[0] == value[-1]
and value[0] in ('"', "'")
):
value = value[1:-1]
if key in BOOL_CONFIGS:
value = to_bool(value)
config[key] = value
rc_file.close()
# backfill no_metrics, handle defaults
# We missed this comment in the 0.5.0 release
# Making it a TODO for 0.6.0
# TODO: remove in 0.6.0
no_metrics_val = config.pop("no_metrics", None)
if no_metrics_val is not None and config.get("enable_metrics") is None:
config["enable_metrics"] = not no_metrics_val
rc = cls.from_dict(config)
return rc
except FileNotFoundError:
return cls.from_dict({}) # type: ignore

View File

@@ -0,0 +1,3 @@
from guardrails.classes.schema.processed_schema import ProcessedSchema
__all__ = ["ProcessedSchema"]

View File

@@ -0,0 +1,19 @@
from dataclasses import dataclass, field
from typing import Any, Dict, List
from guardrails_ai.types import Validator
from guardrails.classes.execution.guard_execution_options import GuardExecutionOptions
from guardrails.classes.output_type import OutputTypes
from guardrails.types.validator import ValidatorMap
@dataclass
class ProcessedSchema:
"""This class is just a container for the various pieces of information we
extract from the various schema wrappers a user can pass in; i.e. RAIL or
Pydantic."""
output_type: OutputTypes = field(default=OutputTypes.STRING)
validators: List[Validator] = field(default_factory=list)
validator_map: ValidatorMap = field(default_factory=dict)
json_schema: Dict[str, Any] = field(default_factory=dict)
exec_opts: GuardExecutionOptions = field(default_factory=GuardExecutionOptions)

View File

@@ -0,0 +1,64 @@
import os
from lxml import etree as ET
class ConstantsContainer:
def __init__(self):
self._constants = {}
self.fill_constants()
def fill_constants(self) -> None:
self_file_path = os.path.dirname(__file__)
self_dirname = os.path.dirname(self_file_path)
constants_file = os.path.abspath(
os.path.join(self_dirname, "..", "constants.xml")
)
with open(constants_file, "r") as f:
xml = f.read()
parser = ET.XMLParser(encoding="utf-8", resolve_entities=False)
parsed_constants = ET.fromstring(xml, parser=parser)
for child in parsed_constants:
if isinstance(child, ET._Comment):
continue
if isinstance(child, str):
continue
constant_name = child.tag
constant_value = child.text
self._constants[constant_name] = constant_value
def __getitem__(self, key):
return self._constants[key]
def __setitem__(self, key, value):
self._constants[key] = value
def __delitem__(self, key):
del self._constants[key]
def __iter__(self):
return iter(self._constants)
def __len__(self):
return len(self._constants)
def __contains__(self, key):
return key in self._constants
def __repr__(self):
return repr(self._constants)
def __str__(self):
return str(self._constants)
def items(self):
return self._constants.items()
def keys(self):
return self._constants.keys()
def values(self):
return self._constants.values()

View File

@@ -0,0 +1,6 @@
import string
class NamespaceTemplate(string.Template):
delimiter = "$"
idpattern = r"[a-z][_a-z0-9.]*"

View File

@@ -0,0 +1,64 @@
from __future__ import annotations
from typing_extensions import deprecated
from typing import Any, Dict
from pydantic import BaseModel
from guardrails_ai.types import (
ValidationResult as IValidationResult,
PassResult as IPassResult,
FailResult as IFailResult,
ErrorSpan as ErrorSpan,
)
def to_validation_result(obj: Any) -> PassResult | FailResult | ValidationResult:
if isinstance(obj, dict):
outcome = obj.get("outcome")
if outcome == "pass":
return PassResult.model_validate(obj)
elif outcome == "fail":
return FailResult.model_validate(obj)
return ValidationResult.model_validate(obj)
class ValidationResult(IValidationResult):
@classmethod
@deprecated("Use to_validation_result() instead.")
def from_interface(cls, i_validation_result: Any) -> "ValidationResult":
return to_validation_result(i_validation_result)
@classmethod
@deprecated("Use to_validation_result instead.")
def from_dict(cls, obj: Any) -> "ValidationResult":
return to_validation_result(obj)
class PassResult(IPassResult, ValidationResult):
@deprecated("Use PassResult.model_dump() instead.")
def to_interface(self) -> dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@deprecated("Use PassResult.model_dump() instead.")
def to_dict(self) -> Dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
class FailResult(IFailResult, ValidationResult):
@classmethod
@deprecated("Use FailResult.model_validate() instead.")
def from_interface(cls, i_fail_result: Any) -> "FailResult":
return cls.model_validate(i_fail_result)
@classmethod
@deprecated("Use FailResult.model_validate() instead.")
def from_dict(cls, obj: Any) -> "FailResult":
return cls.model_validate(obj)
@deprecated("Use FailResult.model_dump() instead.")
def to_dict(self) -> Dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
class StreamValidationResult(BaseModel):
chunk: Any
original_text: str
metadata: Dict[str, Any]

View File

@@ -0,0 +1,56 @@
# TODO Temp to update once generated class is in
from typing import Iterator, List
from guardrails.classes.generic.arbitrary_model import ArbitraryModel
from guardrails_ai.types import FailResult
from guardrails.classes.validation.validator_logs import ValidatorLogs
from guardrails_ai.types import ValidationSummary as IValidationSummary
class ValidationSummary(IValidationSummary, ArbitraryModel):
@staticmethod
def _generate_summaries_from_validator_logs(
validator_logs: List[ValidatorLogs],
) -> Iterator["ValidationSummary"]:
"""Generate a list of ValidationSummary objects from a list of
ValidatorLogs objects.
Using an iterator to allow serializing the summaries to other
formats.
"""
for log in validator_logs:
validation_result = log.validation_result
is_fail_result = isinstance(validation_result, FailResult)
failure_reason = validation_result.error_message if is_fail_result else None
error_spans = validation_result.error_spans if is_fail_result else []
outcome = validation_result.outcome if validation_result else None
yield ValidationSummary(
validatorName=log.validator_name,
validatorStatus=outcome, # type: ignore
propertyPath=log.property_path,
failureReason=failure_reason,
errorSpans=error_spans, # type: ignore
)
@staticmethod
def from_validator_logs(
validator_logs: List[ValidatorLogs],
) -> List["ValidationSummary"]:
summaries = []
for summary in ValidationSummary._generate_summaries_from_validator_logs(
validator_logs
):
summaries.append(summary)
return summaries
@staticmethod
def from_validator_logs_only_fails(
validator_logs: List[ValidatorLogs],
) -> List["ValidationSummary"]:
summaries = []
for summary in ValidationSummary._generate_summaries_from_validator_logs(
validator_logs
):
if summary.failure_reason:
summaries.append(summary)
return summaries

View File

@@ -0,0 +1,91 @@
from datetime import datetime
from typing import Any, Dict, Optional
from typing_extensions import deprecated
from pydantic import Field, field_validator, field_serializer
from guardrails_ai.types import ValidationResult, Outcome, PassResult, FailResult
from guardrails.classes.generic.arbitrary_model import ArbitraryModel
class ValidatorLogs(ArbitraryModel):
"""Logs for a single validator execution."""
validator_name: str = Field(
description="The class name of the validator.", alias="validatorName"
)
registered_name: str = Field(
description="The registry id of the validator.", alias="registeredName"
)
value_before_validation: Any = Field(alias="valueBeforeValidation")
validation_result: Optional[ValidationResult] = Field(
default=None, alias="validationResult"
)
value_after_validation: Optional[Any] = Field(
default=None, alias="valueAfterValidation"
)
start_time: Optional[datetime] = Field(default=None, alias="startTime")
end_time: Optional[datetime] = Field(default=None, alias="endTime")
instance_id: Optional[int] = Field(default=None, alias="instanceId")
property_path: str = Field(
description="The JSON path to the property which was validated that produced"
" this log.",
alias="propertyPath",
)
@field_serializer("start_time")
def serialize_start_time(self, start_time: datetime | None) -> str | None:
if start_time is None:
return None
return start_time.isoformat()
@field_serializer("end_time")
def serialize_end_time(self, end_time: datetime | None) -> str | None:
if end_time is None:
return None
return end_time.isoformat()
# NOTE: It shouldn't be necessary to add this serializer just to call model_dump,
# but it is to get the correct serialized output.
@field_serializer("validation_result")
def serialize_validation_result(
self, validation_result: ValidationResult | None
) -> dict[str, Any] | None:
if validation_result is None:
return None
return validation_result.model_dump(exclude_none=True, by_alias=True)
@field_validator("validation_result", mode="before")
@classmethod
def deserialize_validation_result(
cls, validation_result: Any
) -> ValidationResult | None:
if validation_result is None:
return None
elif isinstance(validation_result, ValidationResult):
return validation_result
elif isinstance(validation_result, dict):
outcome = validation_result.get("outcome")
if outcome == Outcome.PASS:
return PassResult.model_validate(validation_result)
elif outcome == Outcome.FAIL:
return FailResult.model_validate(validation_result)
return ValidationResult.model_validate(validation_result)
@deprecated("Use ValidatorLogs.model_dump() instead.")
def to_interface(self) -> dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@deprecated("Use ValidatorLogs.model_dump() instead.")
def to_dict(self) -> Dict[str, Any]:
return self.model_dump(exclude_none=True, by_alias=True)
@classmethod
@deprecated("Use ValidatorLogs.model_validate() instead.")
def from_interface(cls, i_validator_log: Any) -> "ValidatorLogs":
return cls.model_validate(i_validator_log)
@classmethod
@deprecated("Use ValidatorLogs.model_validate() instead.")
def from_dict(cls, obj: Any) -> "ValidatorLogs":
return cls.model_validate(obj)

View File

@@ -0,0 +1,81 @@
from typing import Iterator, List, Optional, Tuple, Union, Generic, cast
from pydantic import Field
from rich.pretty import pretty_repr
from guardrails_ai.types.validation_outcome import (
ValidationOutcome as IValidationOutcome,
OT,
)
from guardrails.actions.reask import ReAsk
from guardrails.classes.history import Call, Iteration
from guardrails.classes.validation.validation_summary import ValidationSummary
from guardrails.constants import pass_status
from guardrails.utils.safe_get import safe_get
class ValidationOutcome(IValidationOutcome, Generic[OT]):
validation_summaries: Optional[List["ValidationSummary"]] = Field(
description="The summaries of the validation results.",
default=[],
alias="validationSummaries",
)
"""The summaries of the validation results."""
model_config = {
"validate_by_alias": True,
"validate_by_name": True,
"arbitrary_types_allowed": True,
}
@classmethod
def from_guard_history(cls, call: Call):
"""Create a ValidationOutcome from a history Call object."""
last_iteration = call.iterations.last or Iteration(callId=call.id, index=0)
last_output = last_iteration.validation_response or safe_get(
list(last_iteration.reasks), 0
)
validation_passed = call.status == pass_status
validator_logs = last_iteration.validator_logs or []
validation_summaries = ValidationSummary.from_validator_logs_only_fails(
validator_logs
)
reask = last_output if isinstance(last_output, ReAsk) else None
error = call.error
output = cast(OT, call.guarded_output)
return cls(
callId=call.id,
rawLlmOutput=call.raw_outputs.last,
validatedOutput=output,
reask=reask,
validationPassed=validation_passed,
validationSummaries=validation_summaries,
error=error,
)
def __iter__(
self,
) -> Iterator[
Union[Optional[str], Optional[OT], Optional[ReAsk], bool, Optional[str]]
]:
"""Iterate over the ValidationOutcome's fields."""
as_tuple: Tuple[
Optional[str], Optional[OT], Optional[ReAsk], bool, Optional[str]
] = (
self.raw_llm_output,
self.validated_output,
self.reask,
self.validation_passed or False,
self.error,
)
return iter(as_tuple)
def __getitem__(self, keys):
"""Get a subset of the ValidationOutcome's fields."""
return iter(getattr(self, k) for k in keys)
def __str__(self) -> str:
return pretty_repr(self)
def to_dict(self):
return self.model_dump(exclude_none=True, by_alias=True)