-
Notifications
You must be signed in to change notification settings - Fork 27
fix: classify invalid function inputs as user errors instead of runtime crashes #1804
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
4ec33cf
fix: classify invalid function inputs as user errors instead of runti…
mjnovice 3690ceb
chore: bump uipath to 2.13.9
mjnovice b0477b5
Merge remote-tracking branch 'origin/main' into fix/graceful-input-va…
mjnovice c0bbd5c
fix: classify all eval failure-path exceptions, not just wrapped ones
mjnovice c6c7d60
test: cover eval failure-path classification and non-Pydantic input e…
mjnovice File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
packages/uipath/tests/cli/eval/test_runtime_error_classification.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| """Tests for classifying eval execution errors as user vs runtime failures.""" | ||
|
|
||
| import uuid | ||
| from pathlib import Path | ||
| from typing import Any, AsyncGenerator | ||
|
|
||
| from uipath.core.events import EventBus | ||
| from uipath.core.tracing import UiPathTraceManager | ||
| from uipath.eval.helpers import EvalHelpers | ||
| from uipath.eval.runtime import UiPathEvalContext, evaluate | ||
| from uipath.eval.runtime.events import EvalRunUpdatedEvent, EvaluationEvents | ||
| from uipath.eval.runtime.runtime import _is_user_facing_error | ||
| from uipath.runtime import ( | ||
| UiPathExecuteOptions, | ||
| UiPathRuntimeEvent, | ||
| UiPathRuntimeFactorySettings, | ||
| UiPathRuntimeProtocol, | ||
| UiPathRuntimeResult, | ||
| UiPathRuntimeStorageProtocol, | ||
| UiPathStreamOptions, | ||
| ) | ||
| from uipath.runtime.errors import ( | ||
| UiPathErrorCategory, | ||
| UiPathErrorCode, | ||
| UiPathRuntimeError, | ||
| ) | ||
| from uipath.runtime.schema import UiPathRuntimeSchema | ||
|
|
||
|
|
||
| def _make_error(category: UiPathErrorCategory) -> UiPathRuntimeError: | ||
| return UiPathRuntimeError( | ||
| UiPathErrorCode.FUNCTION_EXECUTION_ERROR, | ||
| "Some failure", | ||
| "details", | ||
| category, | ||
| include_traceback=False, | ||
| ) | ||
|
|
||
|
|
||
| def test_user_category_error_is_user_facing(): | ||
| assert _is_user_facing_error(_make_error(UiPathErrorCategory.USER)) is True | ||
|
|
||
|
|
||
| def test_system_category_error_is_not_user_facing(): | ||
| assert _is_user_facing_error(_make_error(UiPathErrorCategory.SYSTEM)) is False | ||
|
|
||
|
|
||
| def test_unknown_category_error_is_not_user_facing(): | ||
| assert _is_user_facing_error(_make_error(UiPathErrorCategory.UNKNOWN)) is False | ||
|
|
||
|
|
||
| def test_plain_exception_is_not_user_facing(): | ||
| assert _is_user_facing_error(ValueError("boom")) is False | ||
|
|
||
|
|
||
| class _FailingRuntime: | ||
| """Runtime whose execution always raises the configured error.""" | ||
|
|
||
| def __init__(self, error: Exception): | ||
| self._error = error | ||
|
|
||
| async def execute( | ||
| self, | ||
| input: dict[str, Any] | None = None, | ||
| options: UiPathExecuteOptions | None = None, | ||
| ) -> UiPathRuntimeResult: | ||
| raise self._error | ||
|
|
||
| async def stream( | ||
| self, | ||
| input: dict[str, Any] | None = None, | ||
| options: UiPathStreamOptions | None = None, | ||
| ) -> AsyncGenerator[UiPathRuntimeEvent, None]: | ||
| raise self._error | ||
| yield # unreachable; makes this an async generator | ||
|
|
||
| async def get_schema(self) -> UiPathRuntimeSchema: | ||
| return UiPathRuntimeSchema( | ||
| filePath="test.py", | ||
| uniqueId="test", | ||
| type="workflow", | ||
| input={"type": "object", "properties": {}}, | ||
| output={"type": "object", "properties": {}}, | ||
| ) | ||
|
|
||
| async def dispose(self) -> None: | ||
| pass | ||
|
|
||
|
|
||
| class _FailingFactory: | ||
| def __init__(self, error: Exception): | ||
| self._error = error | ||
|
|
||
| def discover_entrypoints(self) -> list[str]: | ||
| return ["test"] | ||
|
|
||
| async def get_storage(self) -> UiPathRuntimeStorageProtocol | None: | ||
| return None | ||
|
|
||
| async def get_settings(self) -> UiPathRuntimeFactorySettings | None: | ||
| return None | ||
|
|
||
| async def new_runtime( | ||
| self, entrypoint: str, runtime_id: str, **kwargs | ||
| ) -> UiPathRuntimeProtocol: | ||
| return _FailingRuntime(self._error) | ||
|
|
||
| async def dispose(self) -> None: | ||
| pass | ||
|
|
||
|
|
||
| async def _run_failing_eval(error: Exception) -> list[EvalRunUpdatedEvent]: | ||
| """Run an eval whose execution raises, returning captured run-updated events.""" | ||
| event_bus = EventBus() | ||
| trace_manager = UiPathTraceManager() | ||
| captured: list[EvalRunUpdatedEvent] = [] | ||
|
|
||
| async def capture(event: EvalRunUpdatedEvent) -> None: | ||
| captured.append(event) | ||
|
|
||
| event_bus.subscribe(EvaluationEvents.UPDATE_EVAL_RUN, capture) | ||
|
|
||
| factory = _FailingFactory(error) | ||
| eval_set_path = str(Path(__file__).parent / "evals" / "eval-sets" / "default.json") | ||
| evaluation_set, _ = EvalHelpers.load_eval_set(eval_set_path) | ||
| runtime = await factory.new_runtime("test", "test-runtime-id") | ||
| runtime_schema = await runtime.get_schema() | ||
| evaluators = await EvalHelpers.load_evaluators( | ||
| eval_set_path, evaluation_set, agent_model=None | ||
| ) | ||
|
|
||
| context = UiPathEvalContext() | ||
| context.execution_id = str(uuid.uuid4()) | ||
| context.evaluation_set = evaluation_set | ||
| context.runtime_schema = runtime_schema | ||
| context.evaluators = evaluators | ||
|
|
||
| await evaluate(factory, trace_manager, context, event_bus) | ||
| await event_bus.wait_for_all() | ||
| return captured | ||
|
|
||
|
|
||
| async def test_user_error_execution_is_not_a_runtime_exception(): | ||
| """A USER-category failure is reported unwrapped with runtime_exception=False.""" | ||
| error = UiPathRuntimeError( | ||
| UiPathErrorCode.INPUT_INVALID_JSON, | ||
| "Invalid input", | ||
| "Input does not match the expected schema", | ||
| UiPathErrorCategory.USER, | ||
| include_traceback=False, | ||
| ) | ||
| events = await _run_failing_eval(error) | ||
|
|
||
| failed = [e for e in events if not e.success] | ||
| assert failed | ||
| details = failed[0].exception_details | ||
| assert details is not None | ||
| assert details.exception is error | ||
| assert details.runtime_exception is False | ||
|
|
||
|
|
||
| async def test_unexpected_error_execution_is_a_runtime_exception(): | ||
| """A non-user failure in the execution path is flagged as a runtime exception.""" | ||
| events = await _run_failing_eval(RuntimeError("infrastructure broke")) | ||
|
|
||
| failed = [e for e in events if not e.success] | ||
| assert failed | ||
| details = failed[0].exception_details | ||
| assert details is not None | ||
| assert isinstance(details.exception, RuntimeError) | ||
| assert details.runtime_exception is True | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: spacing is wrong here lint should fail for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spacing here is the standard two blank lines between the import block and the first top-level
def(PEP 8 E303/E302 rules) —ruff checkandruff format --checkboth pass on this file, so lint is green. Happy to adjust if you meant something else!