Skip to content

Commit 5d35331

Browse files
committed
Merge branch 'main' into telemetry-update
2 parents 3d3b31c + bc949cf commit 5d35331

35 files changed

+3745
-1749
lines changed

docs/concepts/parallelization.md renamed to docs/concepts/concurrency.md

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Parallelization
1+
# Concurrency
22
## And the Orchestration of Guard Executions
33

44
This document is a description of the current implementation of the Guardrails' validation loop. It attempts to explain the current patterns used with some notes on why those patterns were accepted at the time of implementation and potential future optimizations. It is _not_ meant to be prescriptive as there can, and will, be improvements made in future versions.
55

66
In general you will find that our approach to performance is two fold:
77
1. Complete computationally cheaper, static checks first and exit early to avoid spending time and resources on more expensive checks that are unlikely to pass when the former fail.
8-
2. Parallelize processing where possible.
8+
2. Run processes concurrently where possible.
99

1010
## Background: The Validation Loop
1111
When a Guard is executed, that is called via `guard()`, `guard.parse()`, `guard.validate()`, etc., it goes through an internal process that has the following steps:
@@ -60,7 +60,7 @@ Besides handling asynchronous calls to the LLM, using an `AsyncGuard` also ensur
6060
* An asyncio event loop is available.
6161
* The asyncio event loop is not taken/already running.
6262
63-
## Validation Orchestration and Parallelization
63+
## Validation Orchestration and Concurrency
6464
6565
### Structured Data Validation
6666
We perform validation with a "deep-first" approach. This has no meaning for unstructured text output since there is only one value, but for structured output it means that the objects are validated from the inside out.
@@ -79,7 +79,7 @@ Take the below structure as an example:
7979
}
8080
```
8181
82-
As of versions v0.4.x and v0.5.x of Guardrails, the above object would validated as follows:
82+
As of versions v0.4.x and v0.5.x of Guardrails, the above object would be validated as follows:
8383
8484
1. foo.baz
8585
2. foo.bez
@@ -88,11 +88,134 @@ As of versions v0.4.x and v0.5.x of Guardrails, the above object would validated
8888
5. bar.buz
8989
6. bar
9090
91-
> NOTE: The approach currently used, and outlined above, was predicated on the assumption that if child properties fail validation, it is unlikely that the parent property would pass. With the current atomic state of validation, it can be argued that this assumption is false. That is, the types of validations applied to parent properties typically take the form of checking the appropriate format of the container like a length check on a list. These types of checks are generally independent of any requirements the child properties have. This opens up the possibility of running all six paths listed above in parallel at once instead of performing them in steps based on key path.
91+
> NOTE: The approach currently used, and outlined above, was predicated on the assumption that if child properties fail validation, it is unlikely that the parent property would pass. With the current atomic state of validation, it can be argued that this assumption is false. That is, the types of validations applied to parent properties typically take the form of checking the appropriate format of the container like a length check on a list. These types of checks are generally independent of any requirements the child properties have. This opens up the possibility of running all six paths listed above concurrently instead of performing them in steps based on key path.
9292
9393
When synchronous validation occurs as defined in [Benefits of AsyncGuard](#benefits-of-async-guard), the validators for each property would be run in the order they are defined on the schema. That also means that any on fail actions are applied in that same order.
9494
95-
When asynchronous validation occurs, there are multiple levels of parallelization possible. First, running validation on the child properties (e.g. `foo.baz` and `foo.bez`) will happen in parallel via the asyncio event loop. Second, within the validation for each property, if the validators have `run_in_separate_process` set to `True`, they are run in parallel via multiprocessing. This multiprocessing is capped to the process count specified by the `GUARDRAILS_PROCESS_COUNT` environment variable which defaults to 10. Note that some environments, like AWS Lambda, may not support multiprocessing in which case you would need to set this environment variable to 1.
95+
When asynchronous validation occurs, there are multiple levels of concurrency possible. First, running validation on the child properties (e.g. `foo.baz` and `foo.bez`) will happen concurrently via the asyncio event loop. Second, the validators on any given property are also run concurrently via the event loop. For validators that only define a synchronous `validate` method, calls to this method are run in the event loops default executor. Note that some environments, like AWS Lambda, may not support multiprocessing in which case you would need to either set the executor to a thread processor instead or limit validation to running synchronously by setting `GUARDRAILS_PROCESS_COUNT=1` or `GUARDRAILS_RUN_SYNC=true`.
9696
9797
### Unstructured Data Validation
98-
When validating unstructured data, i.e. text, the LLM output is treated the same as if it were a property on an object. This means that the validators applied to is have the ability to run in parallel utilizing multiprocessing when `run_in_separate_process` is set to `True` on the validators.
98+
When validating unstructured data, i.e. text, the LLM output is treated the same as if it were a property on an object. This means that the validators applied to is have the ability to run concurrently utilizing the event loop.
99+
100+
### Handling Failures During Async Concurrency
101+
The Guardrails validation loop is opinionated about how it handles failures when running validators concurrently so that it spends the least amount of time processing an output that would result in a failure. It's behavior comes down to when and what it returns based on the [corrective action](/how_to_guides/custom_validators#on-fail) specified on a validator. Corrective actions are processed concurrently since they are specific to a given validator on a given property. This means that interruptive corrective actions, namely `EXCEPTION`, will be the first corrective action enforced because the exception is raised as soon as the failure is evaluated. The remaining actions are handled in the following order after all futures are collected from the validation of a specific property:
102+
1. `FILTER` and `REFRAIN`
103+
2. `REASK`
104+
3. `FIX`
105+
106+
\*_NOTE:_ `NOOP` Does not require any special handling because it does not alter the value.
107+
108+
\*_NOTE:_ `FIX_REASK` Will fall into either the `REASK` or `FIX` bucket based on if the fixed value passes the second round of validation.
109+
110+
This means that if any validator with `on_fail=OnFailAction.EXCEPTION` returns a `FailResult`, then Guardrails will raise a `ValidationError` interrupting the process.
111+
112+
If any validator on a specific property which has `on_fail=OnFailAction.FILTER` or `on_fail=OnFailAction.REFRAIN` returns a `FailResult`, whichever of these is the first to finish will the returned early as the value for that property,
113+
114+
If any validator on a specific property which has `on_fail=OnFailAction.REASK` returns a `FailResult`, all reasks for that property will be merged and a `FieldReAsk` will be returned early as the value for that property.
115+
116+
If any validator on a specific property which has `on_fail=OnFailAction.FIX` returns a `FailResult`, all fix values for that property will be merged and the result of that merge will be returned as the value for that property.
117+
118+
Custom on_fail handlers will fall into one of the above actions based on what it returns; i.e. if it returns an updated value it's considered a `FIX`, if it returns an instance of `Filter` then `FILTER`, etc..
119+
120+
Let's look at an example. We'll keep the validation logic simple and write out some assertions to demonstrate the evaluation order discussed above.
121+
122+
```py
123+
import asyncio
124+
from random import randint
125+
from typing import Optional
126+
from guardrails import AsyncGuard, ValidationOutcome
127+
from guardrails.errors import ValidationError
128+
from guardrails.validators import (
129+
Validator,
130+
register_validator,
131+
ValidationResult,
132+
PassResult,
133+
FailResult
134+
)
135+
136+
@register_validator(name='custom/contains', data_type='string')
137+
class Contains(Validator):
138+
def __init__(self, match_value: str, **kwargs):
139+
super().__init__(
140+
match_value=match_value,
141+
**kwargs
142+
)
143+
self.match_value = match_value
144+
145+
def validate(self, value, metadata = {}) -> ValidationResult:
146+
if self.match_value in value:
147+
return PassResult()
148+
149+
fix_value = None
150+
if self.on_fail_descriptor == 'fix':
151+
# Insert the match_value into the value at a random index
152+
insertion = randint(0, len(value))
153+
fix_value = f"{value[:insertion]}{self.match_value}{value[insertion:]}"
154+
155+
return FailResult(
156+
error_message=f'Value must contain {self.match_value}',
157+
fix_value=fix_value
158+
)
159+
160+
exception_validator = Contains("a", on_fail='exception')
161+
filter_validator = Contains("b", on_fail='filter')
162+
refrain_validator = Contains("c", on_fail='refrain')
163+
reask_validator_1 = Contains("d", on_fail='reask')
164+
reask_validator_2 = Contains("e", on_fail='reask')
165+
fix_validator_1 = Contains("f", on_fail='fix')
166+
fix_validator_2 = Contains("g", on_fail='fix')
167+
168+
guard = AsyncGuard().use_many(
169+
exception_validator,
170+
filter_validator,
171+
refrain_validator,
172+
reask_validator_1,
173+
reask_validator_2,
174+
fix_validator_1,
175+
fix_validator_2
176+
)
177+
178+
### Trigger the exception validator ###
179+
error = None
180+
result: Optional[ValidationOutcome] = None
181+
try:
182+
result = asyncio.run(guard.validate("z", metadata={}))
183+
184+
except ValidationError as e:
185+
error = e
186+
187+
assert result is None
188+
assert error is not None
189+
assert str(error) == "Validation failed for field with errors: Value must contain a"
190+
191+
192+
193+
### Trigger the Filter and Refrain validators ###
194+
result = asyncio.run(guard.validate("a", metadata={}))
195+
196+
assert result.validation_passed is False
197+
# The output was filtered or refrained
198+
assert result.validated_output is None
199+
assert result.reask is None
200+
201+
202+
203+
### Trigger the Reask validator ###
204+
result = asyncio.run(guard.validate("abc", metadata={}))
205+
206+
assert result.validation_passed is False
207+
# If allowed, a ReAsk would have occured
208+
assert result.reask is not None
209+
error_messages = [f.error_message for f in result.reask.fail_results]
210+
assert error_messages == ["Value must contain d", "Value must contain e"]
211+
212+
213+
### Trigger the Fix validator ###
214+
result = asyncio.run(guard.validate("abcde", metadata={}))
215+
216+
assert result.validation_passed is True
217+
# The fix values have been merged
218+
assert "f" in result.validated_output
219+
assert "g" in result.validated_output
220+
print(result.validated_output)
221+
```

docs/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ You can override the `fix` behavior by passing it as a function to the Guard obj
5858
```python
5959
from guardrails import Guard
6060

61-
def fix_is_cake(value, metadata):
61+
def fix_is_cake(value, fail_result: FailResult):
6262
return "IT IS cake"
6363

6464
guard = Guard().use(is_cake, on_fail=fix_is_cake)

docs/how_to_guides/custom_validators.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Validators ship with several out of the box `on_fail` policies. The `OnFailActio
7575
| `OnFailAction.NOOP` | Do nothing. The failure will still be recorded in the logs, but no corrective action will be taken. |
7676
| `OnFailAction.EXCEPTION` | Raise an exception when validation fails. |
7777
| `OnFailAction.FIX_REASK` | First, fix the generated output deterministically, and then rerun validation with the deterministically fixed output. If validation fails, then perform reasking. |
78+
| `OnFailAction.CUSTOM` | This action is set internally when the validator is passed a custom function to handle failures. The function is called with the value that failed validation and the FailResult returned from the Validator. i.e. the custom on fail handler must implement the method signature `def on_fail(value: Any, fail_result: FailResult) -> Any` |
7879

7980
In the code below, a `fix_value` will be supplied in the `FailResult`. This value will represent a programmatic fix that can be applied to the output if `on_fail='fix'` is passed during validator initialization.
8081
```py

docusaurus/sidebars.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const sidebars = {
6666
"concepts/streaming_fixes",
6767
],
6868
},
69-
"concepts/parallelization",
69+
"concepts/concurrency",
7070
"concepts/logs",
7171
"concepts/telemetry",
7272
"concepts/error_remediation",

guardrails/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from guardrails.validator_base import Validator, register_validator
1111
from guardrails.settings import settings
1212
from guardrails.hub.install import install
13+
from guardrails.classes.validation_outcome import ValidationOutcome
1314

1415
__all__ = [
1516
"Guard",
@@ -25,4 +26,5 @@
2526
"Instructions",
2627
"settings",
2728
"install",
29+
"ValidationOutcome",
2830
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from datetime import datetime
2+
from dataclasses import asdict, is_dataclass
3+
from pydantic import BaseModel
4+
from json import JSONEncoder
5+
6+
7+
class DefaultJSONEncoder(JSONEncoder):
8+
def default(self, o):
9+
if hasattr(o, "to_dict"):
10+
return o.to_dict()
11+
elif isinstance(o, BaseModel):
12+
return o.model_dump()
13+
elif is_dataclass(o):
14+
return asdict(o)
15+
elif isinstance(o, set):
16+
return list(o)
17+
elif isinstance(o, datetime):
18+
return o.isoformat()
19+
elif hasattr(o, "__dict__"):
20+
return o.__dict__
21+
return super().default(o)

guardrails/merge.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# SOURCE: https://github.com/spyder-ide/three-merge/blob/master/three_merge/merge.py
2+
from typing import Optional
23
from diff_match_patch import diff_match_patch
34

45
# Constants
@@ -10,7 +11,12 @@
1011
ADDITION = 1
1112

1213

13-
def merge(source: str, target: str, base: str) -> str:
14+
def merge(
15+
source: Optional[str], target: Optional[str], base: Optional[str]
16+
) -> Optional[str]:
17+
if source is None or target is None or base is None:
18+
return None
19+
1420
diff1_l = DIFFER.diff_main(base, source)
1521
diff2_l = DIFFER.diff_main(base, target)
1622

@@ -75,7 +81,7 @@ def merge(source: str, target: str, base: str) -> str:
7581
invariant = ""
7682
target = (target_status, target_text) # type: ignore
7783
if advance:
78-
prev_source_text = source[1]
84+
prev_source_text = source[1] # type: ignore
7985
source = next(diff1, None) # type: ignore
8086
elif len(source_text) < len(target_text):
8187
# Addition performed by source
@@ -119,7 +125,7 @@ def merge(source: str, target: str, base: str) -> str:
119125
invariant = ""
120126
source = (source_status, source_text) # type: ignore
121127
if advance:
122-
prev_target_text = target[1]
128+
prev_target_text = target[1] # type: ignore
123129
target = next(diff2, None) # type: ignore
124130
else:
125131
# Source and target are equal

guardrails/telemetry/open_inference.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Any, Dict, List, Optional
22

3-
from guardrails.telemetry.common import get_span, serialize, to_dict
3+
from guardrails.telemetry.common import get_span, to_dict
4+
from guardrails.utils.serialization_utils import serialize
45

56

67
def trace_operation(

guardrails/telemetry/runner_tracing.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
from guardrails.classes.output_type import OT
1818
from guardrails.classes.validation_outcome import ValidationOutcome
1919
from guardrails.stores.context import get_guard_name
20-
from guardrails.telemetry.common import get_tracer, serialize
20+
from guardrails.telemetry.common import get_tracer
2121
from guardrails.utils.safe_get import safe_get
22+
from guardrails.utils.serialization_utils import serialize
2223
from guardrails.version import GUARDRAILS_VERSION
2324

2425

guardrails/telemetry/validator_tracing.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from functools import wraps
22
from typing import (
33
Any,
4+
Awaitable,
45
Callable,
56
Dict,
67
Optional,
@@ -12,10 +13,11 @@
1213

1314
from guardrails.settings import settings
1415
from guardrails.classes.validation.validation_result import ValidationResult
15-
from guardrails.telemetry.common import get_tracer, serialize
16+
from guardrails.telemetry.common import get_tracer
1617
from guardrails.telemetry.open_inference import trace_operation
1718
from guardrails.utils.casting_utils import to_string
1819
from guardrails.utils.safe_get import safe_get
20+
from guardrails.utils.serialization_utils import serialize
1921
from guardrails.version import GUARDRAILS_VERSION
2022

2123

@@ -138,3 +140,65 @@ def trace_validator_wrapper(*args, **kwargs):
138140
return trace_validator_wrapper
139141

140142
return trace_validator_decorator
143+
144+
145+
def trace_async_validator(
146+
validator_name: str,
147+
obj_id: int,
148+
on_fail_descriptor: Optional[str] = None,
149+
tracer: Optional[Tracer] = None,
150+
*,
151+
validation_session_id: str,
152+
**init_kwargs,
153+
):
154+
def trace_validator_decorator(
155+
fn: Callable[..., Awaitable[Optional[ValidationResult]]],
156+
):
157+
@wraps(fn)
158+
async def trace_validator_wrapper(*args, **kwargs):
159+
if not settings.disable_tracing:
160+
current_otel_context = context.get_current()
161+
_tracer = get_tracer(tracer) or trace.get_tracer(
162+
"guardrails-ai", GUARDRAILS_VERSION
163+
)
164+
validator_span_name = f"{validator_name}.validate"
165+
with _tracer.start_as_current_span(
166+
name=validator_span_name, # type: ignore
167+
context=current_otel_context, # type: ignore
168+
) as validator_span:
169+
try:
170+
resp = await fn(*args, **kwargs)
171+
add_validator_attributes(
172+
*args,
173+
validator_span=validator_span,
174+
validator_name=validator_name,
175+
obj_id=obj_id,
176+
on_fail_descriptor=on_fail_descriptor,
177+
result=resp,
178+
init_kwargs=init_kwargs,
179+
validation_session_id=validation_session_id,
180+
**kwargs,
181+
)
182+
return resp
183+
except Exception as e:
184+
validator_span.set_status(
185+
status=StatusCode.ERROR, description=str(e)
186+
)
187+
add_validator_attributes(
188+
*args,
189+
validator_span=validator_span,
190+
validator_name=validator_name,
191+
obj_id=obj_id,
192+
on_fail_descriptor=on_fail_descriptor,
193+
result=None,
194+
init_kwargs=init_kwargs,
195+
validation_session_id=validation_session_id,
196+
**kwargs,
197+
)
198+
raise e
199+
else:
200+
return await fn(*args, **kwargs)
201+
202+
return trace_validator_wrapper
203+
204+
return trace_validator_decorator

0 commit comments

Comments
 (0)