Skip to content

Commit

Permalink
Merge pull request #995 from roboflow/add-block-id-to-error-payloads
Browse files Browse the repository at this point in the history
Include reference to block or output introducing error
  • Loading branch information
grzegorz-roboflow authored Feb 5, 2025
2 parents 0861cd1 + 6dd299d commit 8de2151
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 16 deletions.
10 changes: 10 additions & 0 deletions inference/core/entities/responses/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
OperationDescription,
OperatorDescription,
)
from inference.core.workflows.errors import WorkflowBlockError
from inference.core.workflows.execution_engine.entities.types import Kind
from inference.core.workflows.execution_engine.introspection.entities import (
BlockDescription,
Expand Down Expand Up @@ -178,3 +179,12 @@ class DescribeInterfaceResponse(BaseModel):
description="Dictionary mapping name of the kind with OpenAPI 3.0 definitions of underlying objects. "
"If list is given, entity should be treated as union of types."
)


class WorkflowErrorResponse(BaseModel):
message: str
error_type: str
context: str
inner_error_type: str
inner_error_message: str
blocks_errors: List[WorkflowBlockError]
42 changes: 31 additions & 11 deletions inference/core/interfaces/http/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from fastapi.responses import JSONResponse, RedirectResponse, Response
from fastapi.staticfiles import StaticFiles
from fastapi_cprofile.profiler import CProfileMiddleware
from pydantic import BaseModel
from starlette.convertors import StringConvertor, register_url_convertor
from starlette.middleware.base import BaseHTTPMiddleware

Expand Down Expand Up @@ -54,7 +53,6 @@
from inference.core.entities.requests.trocr import TrOCRInferenceRequest
from inference.core.entities.requests.workflows import (
DescribeBlocksRequest,
DescribeInterfaceRequest,
PredefinedWorkflowDescribeInterfaceRequest,
PredefinedWorkflowInferenceRequest,
WorkflowInferenceRequest,
Expand Down Expand Up @@ -95,6 +93,7 @@
from inference.core.entities.responses.workflows import (
DescribeInterfaceResponse,
ExecutionEngineVersions,
WorkflowErrorResponse,
WorkflowInferenceResponse,
WorkflowsBlocksDescription,
WorkflowsBlocksSchemaDescription,
Expand Down Expand Up @@ -196,8 +195,6 @@
from inference.core.managers.metrics import get_container_stats
from inference.core.managers.prometheus import InferenceInstrumentator
from inference.core.roboflow_api import (
get_roboflow_dataset_type,
get_roboflow_instant_model_data,
get_roboflow_workspace,
get_workflow_specification,
)
Expand All @@ -215,6 +212,8 @@
NotSupportedExecutionEngineError,
ReferenceTypeError,
RuntimeInputError,
StepExecutionError,
WorkflowBlockError,
WorkflowDefinitionError,
WorkflowError,
WorkflowExecutionEngineVersionError,
Expand Down Expand Up @@ -323,15 +322,17 @@ async def wrapped_route(*args, **kwargs):
WorkflowExecutionEngineVersionError,
NotSupportedExecutionEngineError,
) as error:
content = WorkflowErrorResponse(
message=error.public_message,
error_type=error.__class__.__name__,
context=error.context,
inner_error_type=error.inner_error_type,
inner_error_message=str(error.inner_error),
blocks_errors=error._blocks_errors,
)
resp = JSONResponse(
status_code=400,
content={
"message": error.public_message,
"error_type": error.__class__.__name__,
"context": error.context,
"inner_error_type": error.inner_error_type,
"inner_error_message": str(error.inner_error),
},
content=content.model_dump(),
)
except (
ProcessesManagerInvalidPayload,
Expand Down Expand Up @@ -428,6 +429,25 @@ async def wrapped_route(*args, **kwargs):
},
)
traceback.print_exc()
except StepExecutionError as error:
content = WorkflowErrorResponse(
message=error.public_message,
error_type=error.__class__.__name__,
context=error.context,
inner_error_type=error.inner_error_type,
inner_error_message=str(error.inner_error),
blocks_errors=[
WorkflowBlockError(
block_id=error._block_id,
block_type=error._block_type,
),
],
)
resp = JSONResponse(
status_code=500,
content=content.model_dump(),
)
traceback.print_exc()
except WorkflowError as error:
resp = JSONResponse(
status_code=500,
Expand Down
30 changes: 27 additions & 3 deletions inference/core/workflows/errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
from typing import Optional
from typing import List, Optional

from pydantic import BaseModel, Field


class WorkflowBlockError(BaseModel):
block_id: str
block_type: str
property_name: Optional[str] = None


class WorkflowError(Exception):
Expand Down Expand Up @@ -66,7 +74,14 @@ class WorkflowDefinitionError(WorkflowCompilerError):


class WorkflowSyntaxError(WorkflowDefinitionError):
pass
def __init__(
self,
*args,
blocks_errors: Optional[List[WorkflowBlockError]] = None,
**kwargs,
):
super().__init__(*args, **kwargs)
self._blocks_errors = blocks_errors


class DuplicatedNameError(WorkflowDefinitionError):
Expand Down Expand Up @@ -122,7 +137,16 @@ class InvalidBlockBehaviourError(WorkflowExecutionEngineError):


class StepExecutionError(WorkflowExecutionEngineError):
pass
def __init__(
self,
*args,
block_id: str,
block_type: str,
**kwargs,
):
super().__init__(*args, **kwargs)
self._block_id = block_id
self._block_type = block_type


class ExecutionEngineRuntimeError(WorkflowExecutionEngineError):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing_extensions import Annotated

from inference.core.entities.responses.workflows import WorkflowsBlocksSchemaDescription
from inference.core.workflows.errors import WorkflowSyntaxError
from inference.core.workflows.errors import WorkflowBlockError, WorkflowSyntaxError
from inference.core.workflows.execution_engine.entities.base import InputType, JsonField
from inference.core.workflows.execution_engine.introspection.blocks_loader import (
load_workflow_blocks,
Expand Down Expand Up @@ -58,10 +58,37 @@ def parse_workflow_definition(
outputs=workflow_definition.outputs,
)
except pydantic.ValidationError as e:
blocks_errors = {}
for error in e.errors():
loc = error["loc"]
if loc:
section = loc[0]
if section != "steps":
continue
if len(loc) < 2:
continue

index = loc[1]
element = raw_workflow_definition[section][index]
element_name = element.get("name")
element_type = element.get("type")

property_name = None
if len(loc) > 3 and loc[2] == element_type:
property_name = str(loc[3])

block_error = WorkflowBlockError(
block_id=element_name,
block_type=element_type,
property_name=property_name,
)
blocks_errors[element_name] = block_error

raise WorkflowSyntaxError(
public_message="Could not parse workflow definition. Details provided in inner error.",
context="workflow_compilation | workflow_definition_parsing",
inner_error=e,
blocks_errors=list(blocks_errors.values()),
) from e


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ def safe_execute_step(
except Exception as error:
logger.exception(f"Execution of step {step_selector} encountered error.")
raise StepExecutionError(
public_message=f"Error during execution of step: {step_selector}. Details: {error}",
block_id=step_selector,
block_type=workflow.steps[step_selector.split(".")[-1]].manifest.type,
public_message=error,
context="workflow_execution | step_execution",
inner_error=error,
) from error
Expand Down

0 comments on commit 8de2151

Please sign in to comment.