Source code for pyrit.exceptions.exceptions_helpers

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

import json
import re
import time
import logging

from tenacity import RetryCallState

logger = logging.getLogger(__name__)


def log_exception(retry_state: RetryCallState):
    # Log each retry attempt with exception details at ERROR level
    elapsed_time = time.monotonic() - retry_state.start_time
    call_count = retry_state.attempt_number

    if retry_state.outcome.failed:
        exception = retry_state.outcome.exception()
        logger.error(
            f"Retry attempt {call_count} for {retry_state.fn.__name__} failed with exception: {exception}. "
            f"Elapsed time: {elapsed_time} seconds. Total calls: {call_count}"
        )


def remove_start_md_json(response_msg: str) -> str:
    """
    Checks the message for the listed start patterns and removes them if present.

    Args:
        response_msg (str): The response message to check.

    Returns:
        str: The response message without the start marker (if one was present).
    """

    start_pattern = re.compile(r"^(```json\n|`json\n|```\n|`\n|```json|`json|```|`|json|json\n)")
    match = start_pattern.match(response_msg)
    if match:
        response_msg = response_msg[match.end() :]

    return response_msg


def remove_end_md_json(response_msg: str) -> str:
    """
    Checks the message for the listed end patterns and removes them if present.

    Args:
        response_msg (str): The response message to check.

    Returns:
        str: The response message without the end marker (if one was present).
    """

    end_pattern = re.compile(r"(\n```|\n`|```|`)$")
    match = end_pattern.search(response_msg)
    if match:
        response_msg = response_msg[: match.start()]

    return response_msg


def extract_json_from_string(response_msg: str) -> str:
    """
    Attempts to extract JSON (object or array) from within a larger string, not specific to markdown.

    Args:
        response_msg (str): The response message to check.

    Returns:
        str: The extracted JSON string if found, otherwise the original string.
    """
    json_pattern = re.compile(r"\{.*\}|\[.*\]")
    match = json_pattern.search(response_msg)
    if match:
        return match.group(0)

    return response_msg


[docs] def remove_markdown_json(response_msg: str) -> str: """ Checks if the response message is in JSON format and removes Markdown formatting if present. Args: response_msg (str): The response message to check. Returns: str: The response message without Markdown formatting if present. """ response_msg = remove_start_md_json(response_msg) response_msg = remove_end_md_json(response_msg) # Validate if the remaining response message is valid JSON. If it's still not valid # after removing the markdown notation, try to extract JSON from within the string. try: json.loads(response_msg) return response_msg except json.JSONDecodeError: response_msg = extract_json_from_string(response_msg) try: json.loads(response_msg) return response_msg except json.JSONDecodeError: return "Invalid JSON response: {}".format(response_msg)