Repository: microsoft/promptflow
File: src/promptflow-core/promptflow/core/_prompty_utils.py (Python)

Code before:
                    else:
                        retry_after_seconds = float(retry_after_in_header)
                        msg = (
                            f"{type(e).__name__} #{i}, Retry-After={retry_after_in_header}, "
                            f"Back off {retry_after_seconds} seconds for retry."
                        )
                        logger.warning(msg)
                    time.sleep(retry_after_seconds)
                except OpenAIError as e:
                    # For other non-retriable errors from OpenAIError,
                    # For example, AuthenticationError, APIConnectionError, BadRequestError, NotFoundError
                    # Mark UserError for all the non-retriable OpenAIError
                    logger.error(f"Exception occurs: {type(e).__name__}: {str(e)}")
                    raise WrappedOpenAIError(e)
                except Exception as e:
                    logger.error(f"Exception occurs: {type(e).__name__}: {str(e)}")
                    error_message = f"OpenAI API hits exception: {type(e).__name__}: {str(e)}"
                    raise LLMError(message=error_message)

        return wrapper

    return decorator


def validate_function(common_tsg, i, function, expection: ToolValidationError):
    # validate if the function is a dict
    if not isinstance(function, dict):
        raise expection(message=f"function {i} '{function}' is not a dict. {common_tsg}")
    # validate if has required keys
    for key in ["name", "parameters"]:
        if key not in function.keys():
            raise expection(message=f"function {i} '{function}' does not have '{key}' property. {common_tsg}")
    # validate if the parameters is a dict
    if not isinstance(function["parameters"], dict):
        raise expection(
            message=f"function {i} '{function['name']}' parameters '{function['parameters']}' "
            f"should be described as a JSON Schema object. {common_tsg}"
        )
    # validate if the parameters has required keys
    for key in ["type", "properties"]:
        if key not in function["parameters"].keys():
            raise expection(
                message=f"function {i} '{function['name']}' parameters '{function['parameters']}' "
                f"does not have '{key}' property. {common_tsg}"
            )
    # validate if the parameters type is object
    if function["parameters"]["type"] != "object":
        raise expection(
            message=f"function {i} '{function['name']}' parameters 'type' " f"should be 'object'. {common_tsg}"
        )
    # validate if the parameters properties is a dict

Review comment (suggestion):
I tried to merge this code with the existing handle_openai_error decorator, but found it difficult. So, I created a separate decorator for the async version to keep the code clean

Code after:
                        logger.warning(msg)
                    else:
                        retry_after_seconds = float(retry_after_in_header)
                        msg = (
                            f"{type(e).__name__} #{i}, Retry-After={retry_after_in_header}, "
                            f"Back off {retry_after_seconds} seconds for retry."
                        )
                        logger.warning(msg)
                    time.sleep(retry_after_seconds)
                except OpenAIError as e:
                    # For other non-retriable errors from OpenAIError,
                    # For example, AuthenticationError, APIConnectionError, BadRequestError, NotFoundError
                    # Mark UserError for all the non-retriable OpenAIError
                    logger.error(f"Exception occurs: {type(e).__name__}: {str(e)}")
                    raise WrappedOpenAIError(e)
                except Exception as e:
                    logger.error(f"Exception occurs: {type(e).__name__}: {str(e)}")
                    error_message = f"OpenAI API hits exception: {type(e).__name__}: {str(e)}"
                    raise LLMError(message=error_message)

        return wrapper

    return decorator


def handle_openai_error_async(tries: int = 10, unprocessable_entity_error_tries: int = 3):
    """
    A decorator function for handling OpenAI errors asynchronously.

    OpenAI errors are categorized into retryable and non-retryable.
    For retryable errors, the default is to retry 10 times. The waiting time for each round of retry
    increases exponentially, with a maximum waiting time of 60 seconds.
    The total waiting time for retrying 10 times is about 400s

    For retryable errors, the decorator uses the following parameters to control its retry behavior:
    `tries`: max times for the function invocation, type is int
    `unprocessable_entity_error_tries`: max times for the function invocation when consecutive
        422 error occurs, type is int

    Note:
    - The retry policy for UnprocessableEntityError is different because retrying may not be beneficial,
      so small threshold and requiring consecutive errors.
    """

    def decorator(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            consecutive_422_error_count = 0
            for i in range(tries + 1):
                try:
                    return await func(*args, **kwargs)