Skip to content
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

Include reference to block or output introducing error #995

Merged
merged 12 commits into from
Feb 5, 2025
Merged
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"]
grzegorz-roboflow marked this conversation as resolved.
Show resolved Hide resolved
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