Skip to content

Commit 11442aa

Browse files
authored
Merge pull request #512 from guardrails-ai/exception-handling
Exception handling
2 parents 16da954 + 10bf13d commit 11442aa

File tree

18 files changed

+536
-255
lines changed

18 files changed

+536
-255
lines changed

docs/examples/input_validation.ipynb

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
},
1919
{
2020
"cell_type": "code",
21-
"execution_count": null,
21+
"execution_count": 4,
2222
"metadata": {
2323
"is_executing": true
2424
},
@@ -47,23 +47,34 @@
4747
"source": [
4848
"When `fix` is specified as the on-fail handler, the prompt will automatically be amended before calling the LLM.\n",
4949
"\n",
50-
"In any other case (for example, `exception`), a `ValidationException` will be returned in the outcome."
50+
"In any other case (for example, `exception`), a `ValidatorError` will be returned in the outcome."
5151
]
5252
},
5353
{
5454
"cell_type": "code",
55-
"execution_count": null,
55+
"execution_count": 5,
5656
"metadata": {
5757
"is_executing": true
5858
},
59-
"outputs": [],
59+
"outputs": [
60+
{
61+
"name": "stdout",
62+
"output_type": "stream",
63+
"text": [
64+
"Validation failed for field with errors: must be exactly two words\n"
65+
]
66+
}
67+
],
6068
"source": [
6169
"import openai\n",
70+
"from guardrails.errors import ValidatorError\n",
6271
"\n",
63-
"outcome = guard(\n",
64-
" openai.ChatCompletion.create,\n",
65-
")\n",
66-
"outcome.error"
72+
"try:\n",
73+
" guard(\n",
74+
" openai.ChatCompletion.create,\n",
75+
" )\n",
76+
"except ValidatorError as e:\n",
77+
" print(e)"
6778
]
6879
},
6980
{
@@ -75,9 +86,17 @@
7586
},
7687
{
7788
"cell_type": "code",
78-
"execution_count": null,
89+
"execution_count": 6,
7990
"metadata": {},
80-
"outputs": [],
91+
"outputs": [
92+
{
93+
"name": "stdout",
94+
"output_type": "stream",
95+
"text": [
96+
"Validation failed for field with errors: must be exactly two words\n"
97+
]
98+
}
99+
],
81100
"source": [
82101
"from guardrails.validators import TwoWords\n",
83102
"from pydantic import BaseModel\n",
@@ -91,11 +110,13 @@
91110
"guard = Guard.from_pydantic(Pet)\n",
92111
"guard.with_prompt_validation([TwoWords(on_fail=\"exception\")])\n",
93112
"\n",
94-
"outcome = guard(\n",
95-
" openai.ChatCompletion.create,\n",
96-
" prompt=\"This is not two words\",\n",
97-
")\n",
98-
"outcome.error"
113+
"try:\n",
114+
" guard(\n",
115+
" openai.ChatCompletion.create,\n",
116+
" prompt=\"This is not two words\",\n",
117+
" )\n",
118+
"except ValidatorError as e:\n",
119+
" print(e)"
99120
]
100121
}
101122
],
@@ -115,7 +136,7 @@
115136
"name": "python",
116137
"nbconvert_exporter": "python",
117138
"pygments_lexer": "ipython3",
118-
"version": "3.11.0"
139+
"version": "3.11.6"
119140
}
120141
},
121142
"nbformat": 4,

docs/examples/response_is_on_topic.ipynb

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
},
3838
{
3939
"cell_type": "code",
40-
"execution_count": 20,
40+
"execution_count": 1,
4141
"metadata": {},
4242
"outputs": [],
4343
"source": [
@@ -54,7 +54,7 @@
5454
},
5555
{
5656
"cell_type": "code",
57-
"execution_count": 21,
57+
"execution_count": 2,
5858
"metadata": {},
5959
"outputs": [],
6060
"source": [
@@ -76,7 +76,7 @@
7676
},
7777
{
7878
"cell_type": "code",
79-
"execution_count": 22,
79+
"execution_count": 3,
8080
"metadata": {},
8181
"outputs": [],
8282
"source": [
@@ -115,7 +115,7 @@
115115
},
116116
{
117117
"cell_type": "code",
118-
"execution_count": 23,
118+
"execution_count": 4,
119119
"metadata": {},
120120
"outputs": [
121121
{
@@ -129,6 +129,7 @@
129129
"source": [
130130
"import guardrails as gd\n",
131131
"from guardrails.validators import OnTopic\n",
132+
"from guardrails.errors import ValidatorError\n",
132133
"\n",
133134
"# Create the Guard with the OnTopic Validator\n",
134135
"guard = gd.Guard.from_string(\n",
@@ -146,11 +147,12 @@
146147
")\n",
147148
"\n",
148149
"# Test with a given text\n",
149-
"output = guard.parse(\n",
150-
" llm_output=text,\n",
151-
")\n",
152-
"\n",
153-
"print(output.error)"
150+
"try:\n",
151+
" guard.parse(\n",
152+
" llm_output=text,\n",
153+
" )\n",
154+
"except ValidatorError as e:\n",
155+
" print(e)\n"
154156
]
155157
},
156158
{
@@ -164,7 +166,7 @@
164166
},
165167
{
166168
"cell_type": "code",
167-
"execution_count": 24,
169+
"execution_count": 5,
168170
"metadata": {},
169171
"outputs": [
170172
{
@@ -191,9 +193,12 @@
191193
")\n",
192194
"\n",
193195
"# Test with a given text\n",
194-
"output = guard.parse(llm_output=text)\n",
195-
"\n",
196-
"print(output.error)"
196+
"try:\n",
197+
" guard.parse(\n",
198+
" llm_output=text,\n",
199+
" )\n",
200+
"except ValidatorError as e:\n",
201+
" print(e)"
197202
]
198203
},
199204
{
@@ -207,7 +212,7 @@
207212
},
208213
{
209214
"cell_type": "code",
210-
"execution_count": 25,
215+
"execution_count": 6,
211216
"metadata": {},
212217
"outputs": [
213218
{
@@ -234,11 +239,12 @@
234239
")\n",
235240
"\n",
236241
"# Test with a given text\n",
237-
"output = guard.parse(\n",
238-
" llm_output=text\n",
239-
")\n",
240-
"\n",
241-
"print(output.error)"
242+
"try:\n",
243+
" guard.parse(\n",
244+
" llm_output=text,\n",
245+
" )\n",
246+
"except ValidatorError as e:\n",
247+
" print(e)"
242248
]
243249
}
244250
],
@@ -258,7 +264,7 @@
258264
"name": "python",
259265
"nbconvert_exporter": "python",
260266
"pygments_lexer": "ipython3",
261-
"version": "3.9.17"
267+
"version": "3.11.6"
262268
}
263269
},
264270
"nbformat": 4,

guardrails/classes/history/call.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Dict, Optional, Union
22

3-
from pydantic import Field
3+
from pydantic import Field, PrivateAttr
44
from rich.panel import Panel
55
from rich.pretty import pretty_repr
66
from rich.tree import Tree
@@ -30,19 +30,26 @@ class Call(ArbitraryModel):
3030
inputs: CallInputs = Field(
3131
description="The inputs as passed in to Guard.__call__ or Guard.parse"
3232
)
33+
_exception: Optional[Exception] = PrivateAttr()
3334

3435
# Prevent Pydantic from changing our types
3536
# Without this, Pydantic casts iterations to a list
3637
def __init__(
3738
self,
3839
iterations: Optional[Stack[Iteration]] = None,
3940
inputs: Optional[CallInputs] = None,
41+
exception: Optional[Exception] = None,
4042
):
4143
iterations = iterations or Stack()
4244
inputs = inputs or CallInputs()
43-
super().__init__(iterations=iterations, inputs=inputs) # type: ignore
45+
super().__init__(
46+
iterations=iterations, # type: ignore
47+
inputs=inputs, # type: ignore
48+
_exception=exception, # type: ignore
49+
)
4450
self.iterations = iterations
4551
self.inputs = inputs
52+
self._exception = exception
4653

4754
@property
4855
def prompt(self) -> Optional[str]:
@@ -278,14 +285,18 @@ def validator_logs(self) -> Stack[ValidatorLogs]:
278285
def error(self) -> Optional[str]:
279286
"""The error message from any exception that raised and interrupted the
280287
run."""
281-
if self.iterations.empty():
288+
if self._exception:
289+
return str(self._exception)
290+
elif self.iterations.empty():
282291
return None
283292
return self.iterations.last.error # type: ignore
284293

285294
@property
286295
def exception(self) -> Optional[Exception]:
287296
"""The exception that interrupted the run."""
288-
if self.iterations.empty():
297+
if self._exception:
298+
return self._exception
299+
elif self.iterations.empty():
289300
return None
290301
return self.iterations.last.exception # type: ignore
291302

guardrails/classes/validation_outcome.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from pydantic import Field
44

5-
from guardrails.classes.history import Call
5+
from guardrails.classes.history import Call, Iteration
66
from guardrails.classes.output_type import OT
77
from guardrails.constants import pass_status
88
from guardrails.utils.logs_utils import ArbitraryModel
@@ -32,15 +32,12 @@ class ValidationOutcome(Generic[OT], ArbitraryModel):
3232
error: Optional[str] = Field(default=None)
3333

3434
@classmethod
35-
def from_guard_history(cls, call: Call, error_message: Optional[str]):
36-
last_output = (
37-
call.iterations.last.validation_output
38-
if not call.iterations.empty() and call.iterations.last is not None
39-
else None
40-
)
35+
def from_guard_history(cls, call: Call):
36+
last_iteration = call.iterations.last or Iteration()
37+
last_output = last_iteration.validation_output or last_iteration.parsed_output
4138
validation_passed = call.status == pass_status
4239
reask = last_output if isinstance(last_output, ReAsk) else None
43-
error = call.error or error_message
40+
error = call.error
4441
output = cast(OT, call.validated_output)
4542
return cls(
4643
raw_llm_output=call.raw_outputs.last,

guardrails/datatypes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ def verify_metadata_requirements(
4848
metadata, vars(datatype.children).values()
4949
)
5050
missing_keys.update(nested_missing_keys)
51-
return list(missing_keys)
51+
missing_keys = list(missing_keys)
52+
missing_keys.sort()
53+
return missing_keys
5254

5355

5456
class DataType:

guardrails/errors/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from guardrails.validator_base import ValidatorError
2+
3+
__all__ = ["ValidatorError"]

guardrails/guard.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -422,10 +422,8 @@ def _call_sync(
422422
base_model=self.base_model,
423423
full_schema_reask=full_schema_reask,
424424
)
425-
call, error_message = runner(
426-
call_log=call_log, prompt_params=prompt_params
427-
)
428-
return ValidationOutcome[OT].from_guard_history(call, error_message)
425+
call = runner(call_log=call_log, prompt_params=prompt_params)
426+
return ValidationOutcome[OT].from_guard_history(call)
429427

430428
async def _call_async(
431429
self,
@@ -483,10 +481,10 @@ async def _call_async(
483481
base_model=self.base_model,
484482
full_schema_reask=full_schema_reask,
485483
)
486-
call, error_message = await runner.async_run(
484+
call = await runner.async_run(
487485
call_log=call_log, prompt_params=prompt_params
488486
)
489-
return ValidationOutcome[OT].from_guard_history(call, error_message)
487+
return ValidationOutcome[OT].from_guard_history(call)
490488

491489
def __repr__(self):
492490
return f"Guard(RAIL={self.rail})"
@@ -662,9 +660,9 @@ def _sync_parse(
662660
base_model=self.base_model,
663661
full_schema_reask=full_schema_reask,
664662
)
665-
call, error_message = runner(call_log=call_log, prompt_params=prompt_params)
663+
call = runner(call_log=call_log, prompt_params=prompt_params)
666664

667-
return ValidationOutcome[OT].from_guard_history(call, error_message)
665+
return ValidationOutcome[OT].from_guard_history(call)
668666

669667
async def _async_parse(
670668
self,
@@ -704,11 +702,11 @@ async def _async_parse(
704702
base_model=self.base_model,
705703
full_schema_reask=full_schema_reask,
706704
)
707-
call, error_message = await runner.async_run(
705+
call = await runner.async_run(
708706
call_log=call_log, prompt_params=prompt_params
709707
)
710708

711-
return ValidationOutcome[OT].from_guard_history(call, error_message)
709+
return ValidationOutcome[OT].from_guard_history(call)
712710

713711
def with_prompt_validation(
714712
self,

0 commit comments

Comments
 (0)