Skip to content

Commit b437d85

Browse files
committed
wrap user facing exceptions
1 parent 28355fa commit b437d85

File tree

7 files changed

+156
-124
lines changed

7 files changed

+156
-124
lines changed

guardrails/classes/history/call.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,13 @@ def error(self) -> Optional[str]:
281281
return None
282282
return self.iterations.last.error # type: ignore
283283

284+
@property
285+
def exception(self) -> Optional[Exception]:
286+
"""The exception that interrupted the run."""
287+
if self.iterations.empty():
288+
return None
289+
return self.iterations.last.exception # type: ignore
290+
284291
@property
285292
def failed_validations(self) -> Stack[ValidatorLogs]:
286293
"""The validator logs for any validations that failed during the

guardrails/classes/history/iteration.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ def error(self) -> Optional[str]:
111111
this iteration."""
112112
return self.outputs.error
113113

114+
@property
115+
def exception(self) -> Optional[Exception]:
116+
"""The exception that interrupted this iteration."""
117+
return self.outputs.exception
118+
114119
@property
115120
def failed_validations(self) -> List[ValidatorLogs]:
116121
"""The validator logs for any validations that failed during this

guardrails/classes/history/outputs.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ class Outputs(ArbitraryModel):
4343
"that raised and interrupted the process.",
4444
default=None,
4545
)
46+
exception: Optional[Exception] = Field(
47+
description="The exception that interrupted the process.", default=None
48+
)
4649

4750
def _all_empty(self) -> bool:
4851
return (

guardrails/run.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from guardrails.logger import logger, set_scope
1111
from guardrails.prompt import Instructions, Prompt
1212
from guardrails.schema import Schema, StringSchema
13+
from guardrails.utils.exception_utils import UserFacingException
1314
from guardrails.utils.llm_response import LLMResponse
1415
from guardrails.utils.reask_utils import NonParseableReAsk, ReAsk, reasks_to_dict
1516
from guardrails.validator_base import ValidatorError
@@ -185,9 +186,8 @@ def __call__(
185186
prompt_params=prompt_params,
186187
include_instructions=include_instructions,
187188
)
188-
# TODO decide how to handle errors
189-
except (ValidatorError, ValueError) as e:
190-
raise e
189+
except UserFacingException as e:
190+
raise e.original_exception
191191
except Exception as e:
192192
error_message = str(e)
193193
return call_log, error_message
@@ -273,6 +273,7 @@ def step(
273273
index, raw_output, output_schema
274274
)
275275
if parsing_error:
276+
iteration.outputs.exception = parsing_error
276277
iteration.outputs.error = str(parsing_error)
277278

278279
iteration.outputs.parsed_output = parsed_output
@@ -298,6 +299,7 @@ def step(
298299
except Exception as e:
299300
error_message = str(e)
300301
iteration.outputs.error = error_message
302+
iteration.outputs.exception = e
301303
raise e
302304
return iteration
303305

@@ -322,16 +324,18 @@ def prepare(
322324
"""
323325
with start_action(action_type="prepare", index=index) as action:
324326
if api is None:
325-
raise ValueError("API must be provided.")
327+
raise UserFacingException(ValueError("API must be provided."))
326328

327329
if prompt_params is None:
328330
prompt_params = {}
329331

330332
if msg_history:
331333
if prompt_schema is not None or instructions_schema is not None:
332-
raise ValueError(
333-
"Prompt and instructions validation are "
334-
"not supported when using message history."
334+
raise UserFacingException(
335+
ValueError(
336+
"Prompt and instructions validation are "
337+
"not supported when using message history."
338+
)
335339
)
336340
msg_history = copy.deepcopy(msg_history)
337341
# Format any variables in the message history with the prompt params.
@@ -361,9 +365,11 @@ def prepare(
361365
raise ValidatorError("Message history validation failed")
362366
elif prompt is not None:
363367
if msg_history_schema is not None:
364-
raise ValueError(
365-
"Message history validation is "
366-
"not supported when using prompt/instructions."
368+
raise UserFacingException(
369+
ValueError(
370+
"Message history validation is "
371+
"not supported when using prompt/instructions."
372+
)
367373
)
368374
if isinstance(prompt, str):
369375
prompt = Prompt(prompt)
@@ -417,7 +423,9 @@ def prepare(
417423
)
418424
instructions = Instructions(validated_instructions)
419425
else:
420-
raise ValueError("Prompt or message history must be provided.")
426+
raise UserFacingException(
427+
ValueError("Prompt or message history must be provided.")
428+
)
421429

422430
action.log(
423431
message_type="info",
@@ -692,8 +700,8 @@ async def async_run(
692700
output_schema,
693701
prompt_params=prompt_params,
694702
)
695-
except (ValidatorError, ValueError) as e:
696-
raise e
703+
except UserFacingException as e:
704+
raise e.original_exception
697705
except Exception as e:
698706
error_message = str(e)
699707

@@ -777,6 +785,7 @@ async def async_step(
777785
# Parse: parse the output.
778786
parsed_output, parsing_error = self.parse(index, output, output_schema)
779787
if parsing_error:
788+
iteration.outputs.exception = parsing_error
780789
iteration.outputs.error = str(parsing_error)
781790

782791
iteration.outputs.parsed_output = parsed_output
@@ -801,6 +810,7 @@ async def async_step(
801810
except Exception as e:
802811
error_message = str(e)
803812
iteration.outputs.error = error_message
813+
iteration.outputs.exception = e
804814
raise e
805815
return iteration
806816

guardrails/utils/exception_utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class UserFacingException(Exception):
2+
"""Wraps an exception to denote it as user-facing.
3+
4+
It will be unwrapped in runner.
5+
"""
6+
7+
def __init__(self, original_exception: Exception):
8+
super().__init__()
9+
self.original_exception = original_exception

tests/integration_tests/test_run.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ async def test_sync_async_step_equivalence(mocker):
136136
None,
137137
{},
138138
None,
139+
None,
140+
None,
139141
OUTPUT_SCHEMA,
140142
call_log,
141143
OUTPUT,
@@ -150,6 +152,8 @@ async def test_sync_async_step_equivalence(mocker):
150152
None,
151153
{},
152154
None,
155+
None,
156+
None,
153157
OUTPUT_SCHEMA,
154158
call_log,
155159
OUTPUT,

0 commit comments

Comments
 (0)