Skip to content

Commit

Permalink
[Feature] Improve error handling for loading tool error.
Browse files Browse the repository at this point in the history
  • Loading branch information
codingrabbitt1 committed Sep 19, 2023
1 parent c49c6c8 commit 64944b2
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 89 deletions.
9 changes: 6 additions & 3 deletions src/promptflow/promptflow/_core/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ class PackageToolNotFoundError(ValidationException):
pass


class LoadToolError(ValidationException):
class MissingRequiredInputs(ValidationException):
pass


class MissingRequiredInputs(LoadToolError):
pass
class ToolLoadError(UserErrorException):
"""Exception raised when tool load failed."""

def __init__(self, *, module: str = None):
super().__init__(target=ErrorTarget.TOOL, module=module)


class ToolExecutionError(UserErrorException):
Expand Down
8 changes: 6 additions & 2 deletions src/promptflow/promptflow/_core/tools_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import pkg_resources
import yaml

from promptflow._core._errors import MissingRequiredInputs, NotSupported, PackageToolNotFoundError
from promptflow._core._errors import MissingRequiredInputs, NotSupported, PackageToolNotFoundError, ToolLoadError
from promptflow._core.tool_meta_generator import (
_parse_tool_from_function,
collect_tool_function_in_module,
Expand Down Expand Up @@ -161,7 +161,11 @@ def _load_package_tool(tool_name, module_name, class_name, method_name, node_inp
target=ErrorTarget.EXECUTOR,
)
# Return the init_inputs to update node inputs in the afterward steps
return getattr(provider_class(**init_inputs_values), method_name), init_inputs
try:
api = getattr(provider_class(**init_inputs_values), method_name)
except Exception as ex:
raise ToolLoadError(target=ErrorTarget.TOOL, module=module_name) from ex
return api, init_inputs

@staticmethod
def load_tool_by_api_name(api_name: str) -> Tool:
Expand Down
106 changes: 66 additions & 40 deletions src/promptflow/promptflow/_utils/exception_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,55 +198,44 @@ def build_debug_info(self, ex: Exception):
"innerException": inner_exception,
}

def to_dict(self, *, include_debug_info=False):
"""Return a dict representation of the exception.
This dict specification corresponds to the specification of the Microsoft API Guidelines:
https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#7102-error-condition-responses
@property
def error_codes(self):
"""The hierarchy of the error codes.
Note that this dict representation the "error" field in the response body of the API.
The whole error response is then populated in another place outside of this class.
For general exceptions, the hierarchy should be:
["SystemError", {exception type name}]
"""
if isinstance(self._ex, JsonSerializedPromptflowException):
return self._ex.to_dict(include_debug_info=include_debug_info)

# Otherwise, return general dict representation of the exception.
result = {
"code": infer_error_code_from_class(SystemErrorException),
"message": str(self._ex),
"messageFormat": "",
"messageParameters": {},
"innerError": {
"code": self._ex.__class__.__name__,
"innerError": None,
},
}
if include_debug_info:
result["debugInfo"] = self.debug_info

return result

error_codes: list[str] = [infer_error_code_from_class(SystemErrorException), self._ex.__class__.__name__]
return error_codes

class PromptflowExceptionPresenter(ExceptionPresenter):
@property
def error_code_recursed(self):
"""Returns a dict of the error codes for this exception.
It is populated in a recursive manner, using the source from `error_codes` property.
i.e. For ToolExcutionError which inherits from UserErrorException,
i.e. For PromptflowException, such as ToolExcutionError which inherits from UserErrorException,
The result would be:
{
"code": "UserErrorException",
"code": "UserError",
"innerError": {
"code": "ToolExecutionError",
"innerError": None,
},
}
For other exception types, such as ValueError, the result would be:
{
"code": "SystemError",
"innerError": {
"code": "ValueError",
"innerError": None,
},
}
"""
current_error = None
reversed_error_codes = reversed(self._ex.error_codes) if self._ex.error_codes else []
reversed_error_codes = reversed(self.error_codes) if self.error_codes else []
for code in reversed_error_codes:
current_error = {
"code": code,
Expand All @@ -255,6 +244,52 @@ def error_code_recursed(self):

return current_error

def to_dict(self, *, include_debug_info=False):
"""Return a dict representation of the exception.
This dict specification corresponds to the specification of the Microsoft API Guidelines:
https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#7102-error-condition-responses
Note that this dict representation the "error" field in the response body of the API.
The whole error response is then populated in another place outside of this class.
"""
if isinstance(self._ex, JsonSerializedPromptflowException):
return self._ex.to_dict(include_debug_info=include_debug_info)

# Otherwise, return general dict representation of the exception.
result = {"message": str(self._ex), "messageFormat": "", "messageParameters": {}}
if self.error_code_recursed:
result.update(self.error_code_recursed)
else:
result["code"] = infer_error_code_from_class(SystemErrorException)
result["innerError"] = None
if include_debug_info:
result["debugInfo"] = self.debug_info

return result


class PromptflowExceptionPresenter(ExceptionPresenter):
@property
def error_codes(self):
"""The hierarchy of the error codes.
For subclass of PromptflowException, use the ex.error_codes directly.
For PromptflowException (not a subclass), the ex.error_code is None.
The result should be:
["SystemError", {inner_exception type name if exist}]
"""
if self._ex.error_codes:
return self._ex.error_codes

# For PromptflowException (not a subclass), the ex.error_code is None.
# Handle this case specifically.
error_codes: list[str] = [infer_error_code_from_class(SystemErrorException)]
if self._ex.inner_exception:
error_codes.append(infer_error_code_from_class(self._ex.inner_exception.__class__))
return error_codes

def to_dict(self, *, include_debug_info=False):
result = {
"message": self._ex.message,
Expand All @@ -266,17 +301,8 @@ def to_dict(self, *, include_debug_info=False):
if self.error_code_recursed:
result.update(self.error_code_recursed)
else:
# For PromptflowException (not a subclass), the error_code_recursed is None.
# Handle this case specifically.
result["code"] = infer_error_code_from_class(SystemErrorException)
if self._ex.inner_exception:
# Set the type of inner_exception as the inner error
result["innerError"] = {
"code": infer_error_code_from_class(self._ex.inner_exception.__class__),
"innerError": None,
}
else:
result["innerError"] = None
result["innerError"] = None
if self._ex.additional_info:
result["additionalInfo"] = [{"type": k, "info": v} for k, v in self._ex.additional_info.items()]
if include_debug_info:
Expand Down
48 changes: 47 additions & 1 deletion src/promptflow/promptflow/executor/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------

from promptflow.exceptions import ErrorTarget, SystemErrorException, UserErrorException, ValidationException
from promptflow._utils.exception_utils import ExceptionPresenter, RootErrorCode
from promptflow.exceptions import (
ErrorTarget,
PromptflowException,
SystemErrorException,
UserErrorException,
ValidationException,
)


class InvalidCustomLLMTool(ValidationException):
Expand Down Expand Up @@ -172,3 +179,42 @@ def __init__(self, line_number, timeout):
super().__init__(
message=f"Line {line_number} execution timeout for exceeding {timeout} seconds", target=ErrorTarget.EXECUTOR
)


class ResolveToolError(PromptflowException):
"""Exception raised when tool load failed."""

def __init__(self, *, node_name: str, target: ErrorTarget = ErrorTarget.EXECUTOR, module: str = None):
self._node_name = node_name
super().__init__(target=target, module=module)

@property
def message_format(self):
if self.inner_exception:
return "Tool load failed in '{node_name}': {error_type_and_message}"
else:
return "Tool load failed in '{node_name}'."

@property
def message_parameters(self):
error_type_and_message = None
if self.inner_exception:
error_type_and_message = f"({self.inner_exception.__class__.__name__}) {self.inner_exception}"

return {
"node_name": self._node_name,
"error_type_and_message": error_type_and_message,
}

@property
def additional_info(self):
"""Get additional info from innererror when the innererror is PromptflowException"""
if isinstance(self.inner_exception, PromptflowException):
return self.inner_exception.additional_info
return None

@property
def error_codes(self):
if self.inner_exception:
return ExceptionPresenter.create(self.inner_exception).error_codes
return [RootErrorCode.SYSTEM_ERROR]
56 changes: 30 additions & 26 deletions src/promptflow/promptflow/executor/_tool_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,19 @@
from typing import Callable, List, Optional

from promptflow._core.connection_manager import ConnectionManager
from promptflow._core.tools_manager import (
BuiltinsManager,
ToolLoader,
connection_type_to_api_mapping,
)
from promptflow._core.tools_manager import BuiltinsManager, ToolLoader, connection_type_to_api_mapping
from promptflow._utils.tool_utils import get_inputs_for_prompt_template, get_prompt_param_name_from_func
from promptflow.contracts.flow import InputAssignment, InputValueType, Node, ToolSourceType
from promptflow.contracts.tool import ConnectionType, Tool, ToolType, ValueType
from promptflow.contracts.types import PromptTemplate
from promptflow.exceptions import ErrorTarget, UserErrorException
from promptflow.exceptions import ErrorTarget, PromptflowException, UserErrorException
from promptflow.executor._errors import (
ConnectionNotFound,
InvalidConnectionType,
InvalidCustomLLMTool,
InvalidSource,
NodeInputValidationError,
ResolveToolError,
ValueTypeUnresolved,
)

Expand Down Expand Up @@ -97,26 +94,33 @@ def _convert_node_literal_input_types(self, node: Node, tool: Tool):
return updated_node

def resolve_tool_by_node(self, node: Node, convert_input_types=True) -> ResolvedTool:
if node.source is None:
raise UserErrorException(f"Node {node.name} does not have source defined.")

if node.type is ToolType.PYTHON:
if node.source.type == ToolSourceType.Package:
return self._resolve_package_node(node, convert_input_types=convert_input_types)
elif node.source.type == ToolSourceType.Code:
return self._resolve_script_node(node, convert_input_types=convert_input_types)
raise NotImplementedError(f"Tool source type {node.source.type} for python tool is not supported yet.")
elif node.type is ToolType.PROMPT:
return self._resolve_prompt_node(node)
elif node.type is ToolType.LLM:
return self._resolve_llm_node(node, convert_input_types=convert_input_types)
elif node.type is ToolType.CUSTOM_LLM:
if node.source.type == ToolSourceType.PackageWithPrompt:
resolved_tool = self._resolve_package_node(node, convert_input_types=convert_input_types)
return self._integrate_prompt_in_package_node(node, resolved_tool)
raise NotImplementedError(f"Tool source type {node.source.type} for custom_llm tool is not supported yet.")
else:
raise NotImplementedError(f"Tool type {node.type} is not supported yet.")
try:
if node.source is None:
raise UserErrorException(f"Node {node.name} does not have source defined.")

if node.type is ToolType.PYTHON:
if node.source.type == ToolSourceType.Package:
return self._resolve_package_node(node, convert_input_types=convert_input_types)
elif node.source.type == ToolSourceType.Code:
return self._resolve_script_node(node, convert_input_types=convert_input_types)
raise NotImplementedError(f"Tool source type {node.source.type} for python tool is not supported yet.")
elif node.type is ToolType.PROMPT:
return self._resolve_prompt_node(node)
elif node.type is ToolType.LLM:
return self._resolve_llm_node(node, convert_input_types=convert_input_types)
elif node.type is ToolType.CUSTOM_LLM:
if node.source.type == ToolSourceType.PackageWithPrompt:
resolved_tool = self._resolve_package_node(node, convert_input_types=convert_input_types)
return self._integrate_prompt_in_package_node(node, resolved_tool)
raise NotImplementedError(
f"Tool source type {node.source.type} for custom_llm tool is not supported yet."
)
else:
raise NotImplementedError(f"Tool type {node.type} is not supported yet.")
except Exception as e:
if isinstance(e, PromptflowException) and e.target != ErrorTarget.UNKNOWN:
raise ResolveToolError(node_name=node.name, target=e.target, module=e.module) from e
raise ResolveToolError(node_name=node.name) from e

def _load_source_content(self, node: Node) -> str:
source = node.source
Expand Down
Loading

0 comments on commit 64944b2

Please sign in to comment.