참고소스 수정본
This commit is contained in:
21
참고/guardrails-main/guardrails/classes/__init__.py
Normal file
21
참고/guardrails-main/guardrails/classes/__init__.py
Normal 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",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
from guardrails.classes.execution.guard_execution_options import GuardExecutionOptions
|
||||
|
||||
__all__ = ["GuardExecutionOptions"]
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
114
참고/guardrails-main/guardrails/classes/generic/stack.py
Normal file
114
참고/guardrails-main/guardrails/classes/generic/stack.py
Normal 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)
|
||||
@@ -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"]
|
||||
459
참고/guardrails-main/guardrails/classes/history/call.py
Normal file
459
참고/guardrails-main/guardrails/classes/history/call.py
Normal 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)
|
||||
128
참고/guardrails-main/guardrails/classes/history/call_inputs.py
Normal file
128
참고/guardrails-main/guardrails/classes/history/call_inputs.py
Normal 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)
|
||||
106
참고/guardrails-main/guardrails/classes/history/inputs.py
Normal file
106
참고/guardrails-main/guardrails/classes/history/inputs.py
Normal 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)
|
||||
234
참고/guardrails-main/guardrails/classes/history/iteration.py
Normal file
234
참고/guardrails-main/guardrails/classes/history/iteration.py
Normal 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)
|
||||
193
참고/guardrails-main/guardrails/classes/history/outputs.py
Normal file
193
참고/guardrails-main/guardrails/classes/history/outputs.py
Normal 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)
|
||||
5
참고/guardrails-main/guardrails/classes/input_type.py
Normal file
5
참고/guardrails-main/guardrails/classes/input_type.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from typing import TypeVar
|
||||
|
||||
from langchain_core.messages import BaseMessage
|
||||
|
||||
InputType = TypeVar("InputType", str, BaseMessage)
|
||||
142
참고/guardrails-main/guardrails/classes/llm/llm_response.py
Normal file
142
참고/guardrails-main/guardrails/classes/llm/llm_response.py
Normal 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)
|
||||
45
참고/guardrails-main/guardrails/classes/llm/prompt_callable.py
Normal file
45
참고/guardrails-main/guardrails/classes/llm/prompt_callable.py
Normal 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
|
||||
63
참고/guardrails-main/guardrails/classes/output_type.py
Normal file
63
참고/guardrails-main/guardrails/classes/output_type.py
Normal 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")
|
||||
80
참고/guardrails-main/guardrails/classes/rc.py
Normal file
80
참고/guardrails-main/guardrails/classes/rc.py
Normal 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
|
||||
3
참고/guardrails-main/guardrails/classes/schema/__init__.py
Normal file
3
참고/guardrails-main/guardrails/classes/schema/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from guardrails.classes.schema.processed_schema import ProcessedSchema
|
||||
|
||||
__all__ = ["ProcessedSchema"]
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -0,0 +1,6 @@
|
||||
import string
|
||||
|
||||
|
||||
class NamespaceTemplate(string.Template):
|
||||
delimiter = "$"
|
||||
idpattern = r"[a-z][_a-z0-9.]*"
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
81
참고/guardrails-main/guardrails/classes/validation_outcome.py
Normal file
81
참고/guardrails-main/guardrails/classes/validation_outcome.py
Normal 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)
|
||||
Reference in New Issue
Block a user