import re

def has_sequential_chars(string1, string2, n):
    """
    Check if any n sequential characters from string1 appear in string2.

    Args:
    string1 (str): The first string to check.
    string2 (str): The second string in which to look for sequences.
    n (int): The length of the sequence to check.

    Returns:
    bool: True if any n sequential characters from string1 are found in string2, False otherwise.
    """

    # Check if n is larger than the length of string1.
    if n > len(string1):
        return False, ""

    # Iterate over string1 and check for each n-length substring in string2
    comparison_string = ""
    for i in range(len(string1) - n + 1):
        comparison_string = string1[i : i + n]
        if comparison_string in string2:
            return True, comparison_string

    return False, comparison_string

def extract_conversation(conversation):
    """
    Extracts conversation from a string and returns it as a list of tuples.

    Parameters:
    conversation (str): A string representing the conversation.

    Returns:
    list of tuples: Each tuple contains the character's name and their message.
    """
    lines = conversation.strip().split("\n")
    dialogues = []

    for line in lines:
        if ":" in line:
            # Splitting at the first occurrence of ':'
            parts = line.split(":", 1)
            charname = parts[0].strip()
            message = parts[1].strip() if len(parts) > 1 else ""
            dialogues.append((charname, message))

    return dialogues

def compare_answers_with_qatuples(dialogues, qatuples, n):
    """
    Compares each answer in dialogues with the corresponding answer from qatuples.

    Parameters:
    dialogues (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n (int): Number of sequential characters to check.

    Returns:
    bool: True if all answers match the corresponding answers in qatuples, False otherwise.
    """
    for i in range(
        2, len(dialogues), 2
    ):  # Answers are at even indices, starting from 2
        if int(i / 2) - 1 >= len(
            qatuples
        ):  # at this point we've reached added stuff that doesn't have a corresponding qatuple
            break
        sequential, comp = has_sequential_chars(
            qatuples[int(i / 2) - 1][1], dialogues[i][1], n
        )
        print(sequential)
        print(n)
        if not sequential:
            print(
                f"Answer {int(i/2)}: {dialogues[i][1]} does not match the corresponding answer in qatuples: {qatuples[int(i/2) - 1][1]}, {comp}"
            )
            return False
    return True

def check_for_repeated_dialogue_answers(dialogues, qatuples, n):
    """
    Checks each line of dialogue to ensure that it does not repeat the corresponding answer from qatuples.

    Parameters:
    dialogues (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n (int): Number of sequential characters to check for repetition.

    Returns:
    bool: True if no dialogue line repeats its corresponding answer, False otherwise.
    """
    for i in range(
        2, len(dialogues), 2
    ):  # Answers are at even indices, starting from 2
        if int(i / 2) - 1 >= len(
            qatuples
        ):  # at this point we've reached added stuff that doesn't have a corresponding qatuple
            break

        dialogue_answer = dialogues[i][1]
        corresponding_qatuple_answer = qatuples[int(i / 2) - 1][1]
        # Check if the dialogue answer repeats the qatuple answer
        if dialogue_answer.count(corresponding_qatuple_answer) > 1:
            return False
    return True

# def check_repeated_answer(dialogues, qatuples):
#     # Get the length of the dialogues
#     conv_length = len(dialogues)

#     # Loop through even indices starting from 2 (first answer is at index 2)
#     for i in range(2, conv_length, 2):
#         current_answer = dialogues[i][1][:n_characters_same]
#         next_answer_index = i + 2

#         if next_answer_index < conv_length:
#             next_answer = dialogues[next_answer_index][1][:n_characters_same]
#             if current_answer == next_answer:
#                 return False
#     return True

def check_conversation_length(conv, qatuples):
    """Checks the length of the conversation"""
    # Dialogues with answers should be at even indices that are not 0
    # qatuples are of the format (question, answer,source_text,name_of_text) -- only the first two are used here

    # Get the length of the dialogues
    conv_length = len(conv)

    target_length = len(qatuples) * 2 + 1
    if (
        conv_length < target_length
    ):  # we can have more messages since the AI might add some stuff at the end to wrap up the scene
        return False
    else:
        return True

def check_conversation_for_text_from_examples(conv):
    """Checks if certain strings from the few-shot examples appear in the conversation"""
    strings_to_check_for = [
        "her lipstick-colored lips",
        "coquettishly tilts her head to the side,",
        "Awwww, you're no fun,",
        "Reminds me of my colleagues...",
        "" "I'll see you at that cafe.",
        "Ghh... you know,",
        "you're breaking a poor woman's heart,",
        "surprising innocence and warmth",
        'in mock-thought, "',
        " _I can't believe my ears. Did ",
    ]
    matches_found = 0
    for string in strings_to_check_for:
        if string in conv:
            matches_found += 1
            print(f"Found {string} in the conversation!")
    if matches_found > 2:
        print(
            f"Found {matches_found} matches for strings from the few-shot examples. Validation failed!"
        )
        return False
    return True

def check_each_question_contains_q_from_tuples(conv, qatuples, n):
    """
    Ensures that each question contains at least n sequential characters from the corresponding question in qatuples.
    If the first question fails this check, return None for special handling.

    Parameters:
    conv (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n (int): Number of sequential characters to check.

    Returns:
    bool or None: True if all questions pass the check, False if any fail, None if the first question fails.
    """
    for i in range(1, len(conv), 2):  # Questions are at odd indices
        if i // 2 < len(
            qatuples
        ):  # Ensure we only check questions that have corresponding qatuples
            question_from_conv = conv[i][1]
            question_from_tuples = qatuples[i // 2][0]
            # print(question_from_tuples, question_from_conv)
            sequential, _ = has_sequential_chars(
                question_from_tuples, question_from_conv, n
            )
            if not sequential:
                if i == 1:
                    return None  # Special handling for the first question
                else:
                    return False
    return True

def check_for_unintended_repeated_quotes(dialogues, qatuples, n_characters_shared):
    """
    Checks if answers in the conversation inadvertently use a long quote from another QA pair.

    Args:
    dialogues (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n_characters_shared (int): Number of sequential characters to check for repetition.

    Returns:
    bool: True if no unintended repeated quotes are found, False otherwise.
    """

    # Extract only the answers from the QA tuples for comparison
    qa_answers = [qa[1] for qa in qatuples]

    for i in range(
        2, len(dialogues), 2
    ):  # Answers are at even indices, starting from 2
        # Skip if there's no corresponding QA tuple
        if int(i / 2) - 1 >= len(qatuples):
            break

        dialogue_answer = dialogues[i][1]
        corresponding_qa_answer = qatuples[int(i / 2) - 1][1]

        # Check for each answer in the QA tuples
        for idx, qa_answer in enumerate(qa_answers):
            # Skip the comparison for the current QA pair itself
            if qa_answer == corresponding_qa_answer:
                continue

            # Check if the dialogue answer contains a long quote from another QA answer
            sequential, comp_string = has_sequential_chars(
                qa_answer, dialogue_answer, n_characters_shared
            )
            if sequential:
                if comp_string in corresponding_qa_answer:
                    continue  # This is a quote from the corresponding answer, so it's fine
                else:
                    # Found an unintended repeated quote
                    return False
    return True

def call_all_processors(multiturn_conversation, qatuples):
    convs_split = extract_conversation(multiturn_conversation)

    # Check if answers in dialogues match corresponding answers in qatuples
    if not compare_answers_with_qatuples(convs_split, qatuples, 15):
        print("Answers in dialogues do not match corresponding answers in qatuples.")
        return False

    # Check if any dialogue line repeats its corresponding answer
    if not check_for_repeated_dialogue_answers(convs_split, qatuples, 15):
        print("Dialogue line repeats its corresponding answer.")
        return False

    # Check the conversation length
    if not check_conversation_length(convs_split, qatuples):
        print("Conversation is too short! Validation failed!")
        return False

    # Check for text from examples (assuming this is implemented elsewhere)
    if not check_conversation_for_text_from_examples(multiturn_conversation):
        print("Conversation does not contain text from examples. Validation failed!")
        return False

    # Check for unintended repeated quotes
    if not check_for_unintended_repeated_quotes(convs_split, qatuples, 100):
        print("Conversation contains unintended repeated quotes. Validation failed!")
        return False

    # Check each question contains a part of the question from tuples
    result = check_each_question_contains_q_from_tuples(convs_split, qatuples, 15)
    if result is None:
        print(
            "First question does not contain a part of the question from tuples. Validation failed!"
        )
        return None
    elif not result:
        print(
            "Each question does not contain a part of the question from tuples. Validation failed!"
        )
        return False

    # If all checks pass
    return True

if __name__ == "__main__":
    # Test cases for has_sequential_chars
    print("Testing has_sequential_chars:")
    print(has_sequential_chars("hello", "worldhello", 3))  #
    print("Expected True")
    print(has_sequential_chars("abc", "defghijkl", 2))  # Expected False
    print("Expected False")
    print(has_sequential_chars("", "empty", 1))  # Expected False (empty string1)
    print("Expected False")
    print(
        has_sequential_chars("longstring", "short", 5)
    )  # Expected False (n is longer than string2)
    print("Expected False")
    print(
        has_sequential_chars("overlap", "laptopp", 3)
    )  # Expected True (partial overlap)
    print("Expected True")

    # Test cases for extract_conversation
    print("\nTesting extract_conversation:")
    test_conversation1 = "Charname1: Hello\nCharname2: Hi\nCharname3: How are you?"
    print(
        extract_conversation(test_conversation1)
    )  # Expected [('Charname1', 'Hello'), ('Charname2', 'Hi'), ('Charname3', 'How are you?')]
    print(
        "Expected [('Charname1', 'Hello'), ('Charname2', 'Hi'), ('Charname3', 'How are you?')]"
    )
    test_conversation2 = "No colons here"
    print(extract_conversation(test_conversation2))  # Expected []
    print("Expected []")
    test_conversation3 = ""
    print(extract_conversation(test_conversation3))  # Expected []
    print("Expected []")

    # Test cases for compare_answers_with_qatuples
    print("\nTesting compare_answers_with_qatuples:")
    dialogues1 = [
        ("Charname1", "Hello"),
        ("Charname2", "Hi how are you"),
        "Totally Fantastic and Amazing!",
    ]
    qatuples1 = [("How are you?", "Fine")]
    print(compare_answers_with_qatuples(dialogues1, qatuples1, 2))  # Expected False
    print("Expected False")
    dialogues2 = [
        ("Charname1", "Hello"),
        ("Charname2", "Hi how are you"),
        ("Charname1", "Mostly Fine I think, yeah"),
    ]
    print(compare_answers_with_qatuples(dialogues2, qatuples1, 2))  # Expected True
    print("Expected True")
    dialogues3 = []
    qatuples2 = []
    print(
        compare_answers_with_qatuples(dialogues3, qatuples2, 2)
    )  # Expected True (both empty)
    print("Expected True (both empty)")

    # Test cases for check_for_repeated_dialogue_answers
    print("\nTesting check_for_repeated_dialogue_answers:")
    qatuples_repeated_answers = [("How are you?", "Fine, thank you for asking!")]
    dialogues4 = [
        ("Charname1", "Hello"),
        ("Charname2", "How are you?"),
        ("Charname1", "Fine, thank you for asking!"),
    ]
    print(
        check_for_repeated_dialogue_answers(dialogues4, qatuples_repeated_answers, 2)
    )  # Expected True (no repetition)
    print("Expected True (no repetition)")
    dialogues5 = [
        ("Charname1", "Hello"),
        ("Charname2", "How are you?"),
        (
            "Charname1",
            "Fine, thank you for asking! It's nice today, after all, so I'm Fine, thank you for asking!",
        ),
    ]
    print(
        check_for_repeated_dialogue_answers(dialogues5, qatuples_repeated_answers, 2)
    )  # Expected False (repetition)
    print("Expected False (repetition)")

    # Test cases for check_repeated_answer
    # print("\nTesting check_repeated_answer:")
    # dialogues6 = [("Charname1", "Question"), ("Charname2", "Answer1"), ("Charname3", "Question"), ("Charname4", "Answer1")]
    # print(check_repeated_answer(dialogues6))  # Expected False (repeated answers)
    # dialogues7 = [("Charname1", "Question"), ("Charname2", "Answer1"), ("Charname3", "Question"), ("Charname4", "Answer2")]
    # print(check_repeated_answer(dialogues7))  # Expected True (different answers)

    # Test cases for check_conversation_length
    print("\nTesting check_conversation_length:")
    conv1 = [("Charname1", "Hello"), ("Charname2", "Hi, How are you?")]
    print(
        check_conversation_length(conv1, qatuples1)
    )  # Expected False (conversation too short)
    print("Expected False (conversation too short)")
    conv2 = [("Charname1", "Hello"), ("Charname2", "Hi"), ("Charname3", "How are you?")]
    print(check_conversation_length(conv2, qatuples1))  # Expected True (correct length)
    print("Expected True (correct length)")

    # Test cases for check_conversation_for_text_from_examples (commented out as implementation is assumed elsewhere)
    # print("\nTesting check_conversation_for_text_from_examples:")
    # conv3 = "This conversation contains lipstick-colored lips and a coquettishly tilting head."
    # print(check_conversation_for_text_from_examples(conv3))  # Expected False (contains example texts)

    # Test cases for check_each_question_contains_q_from_tuples
    print("\nTesting check_each_question_contains_q_from_tuples:")
    conv4 = [
        ("Charname2", "Hiya~!"),
        ("Charname1", "What's your favorite color?"),
        ("Charname2", "I'm Fine, thank you very much!"),
    ]
    print(check_each_question_contains_q_from_tuples(conv4, qatuples1, 6))
    print("Expected None (no matching question, first Q)")

    conv45 = [
        ("Charname2", "Hiya~!"),
        ("Charname1", "How are you?"),
        ("Charname2", "I'm Fine, thank you very much!"),
        ("Charname1", "What is the airspeed velocity of an unladen swallow?"),
        ("Charname2", "Black, like my soul."),
    ]
    qatuples3 = [
        ("How are you?", "I'm Fine, thank you very much!"),
        ("What's your favorite color?", "Black, like my soul."),
    ]
    print(check_each_question_contains_q_from_tuples(conv45, qatuples3, 6))
    print("Expected False (no matching question, second Q)")

    conv5 = [
        ("Charname1", "Hiya~!"),
        ("Charname2", "How are you?"),
        ("Charname2", "I'm Fine, thank you very much!"),
        ("Charname1", "What's your favorite color?"),
        ("Charname2", "Black, like my soul."),
    ]
    print(check_each_question_contains_q_from_tuples(conv5, qatuples1 + [], 6))  #
    print("Expected True (question contains part of qatuple question)")

    # Test cases for check_for_unintended_repeated_quotes
    print("\nTesting check_for_unintended_repeated_quotes:")
    # Creating a set of dialogues and qatuples where there is an unintended repeated quote
    qatuples_shared = [
        ("What is your favorite book?", "I love reading The Hobbit."),
        (
            "Tell me about a recent happy moment.",
            "My friends threw me a surprise party!",
        ),
    ]
    dialogues_shared1 = [
        ("Charname1", "Hello"),
        ("Charname2", "What is your favorite book?"),
        ("Charname1", "I love reading The Hobbit."),
        ("Charname2", "Tell me about a recent happy moment."),
        (
            "Charname1",
            "My friends threw me a surprise party! It felt just like I was in The Hobbit.",
        ),
    ]
    print(
        check_for_unintended_repeated_quotes(dialogues_shared1, qatuples_shared, 10)
    )  # Expected False (repeated long quote from another answer)
    print("Expected False (repeated long quote from another answer)")

    # Creating a set of dialogues and qatuples where there are no unintended repeated quotes
    dialogues_shared2 = [
        ("Charname1", "Hello"),
        ("Charname2", "What is your favorite book?"),
        ("Charname1", "I absolutely adore The Lord of the Rings."),
        ("Charname2", "Tell me about a recent happy moment."),
        ("Charname1", "I had a great time at the beach last weekend!"),
    ]
    print(
        check_for_unintended_repeated_quotes(dialogues_shared2, qatuples_shared, 10)
    )  # Expected True (no repeated long quotes)
    print("Expected True (no repeated long quotes)")

    # Test cases for call_all_processors
    print("\nTesting call_all_processors:")
    complete_conversation = """
    Charname1: Hello
    Charname2: How are you doing today?
    Charname1: I'm fine, thank you very much!
    Charname2: What's the weather like?
    Charname1: It's sunny and warm. I don't like sand. It's coarse and rough and irritating and it gets everywhere.
    Foo: Bar
    Baz: Quux
    """
    qatuples_complete = [
        ("How are you doing today?", "I'm fine, thank you very much!"),
        (
            "What's the weather like?",
            "It's sunny and warm. I don't like sand. It's coarse and rough and irritating and it gets everywhere.",
        ),
    ]
    print(call_all_processors(complete_conversation, qatuples_complete))  #
    print("Expected True (all checks pass)")
    incomplete_conversation = """
    Charname1: How's it going?
    Charname2: Good.
    Charname1: Any plans?
    Charname2: None.
    Foo: Bar
    Baz: Quux
    """
    print(call_all_processors(incomplete_conversation, qatuples_complete))  #
    print("Expected False (checks fail)")


import re

def has_sequential_chars(string1, string2, n):
    """
    Check if any n sequential characters from string1 appear in string2.

    Args:
    string1 (str): The first string to check.
    string2 (str): The second string in which to look for sequences.
    n (int): The length of the sequence to check.

    Returns:
    bool: True if any n sequential characters from string1 are found in string2, False otherwise.
    """

    # Check if n is larger than the length of string1.
    if n > len(string1):
        return False, ""

    # Iterate over string1 and check for each n-length substring in string2
    comparison_string = ""
    for i in range(len(string1) - n + 1):
        comparison_string = string1[i : i + n]
        if comparison_string in string2:
            return True, comparison_string

    return False, comparison_string

def extract_conversation(conversation):
    """
    Extracts conversation from a string and returns it as a list of tuples.

    Parameters:
    conversation (str): A string representing the conversation.

    Returns:
    list of tuples: Each tuple contains the character's name and their message.
    """
    lines = conversation.strip().split("\n")
    dialogues = []

    for line in lines:
        if ":" in line:
            # Splitting at the first occurrence of ':'
            parts = line.split(":", 1)
            charname = parts[0].strip()


    n (int): The length of the sequence to check.

    Returns:
    bool: True if any n sequential characters from string1 are found in string2, False otherwise.
    """

    # Check if n is larger than the length of string1.
    if n > len(string1):
        return False, ""

    # Iterate over string1 and check for each n-length substring in string2
    comparison_string = ""
    for i in range(len(string1) - n + 1):
        comparison_string = string1[i : i + n]
        if comparison_string in string2:
            return True, comparison_string

    return False, comparison_string

def extract_conversation(conversation):
    """
    Extracts conversation from a string and returns it as a list of tuples.

    Parameters:
    conversation (str): A string representing the conversation.

    Returns:
    list of tuples: Each tuple contains the character's name and their message.
    """
    lines = conversation.strip().split("\n")
    dialogues = []

    for line in lines:
        if ":" in line:
            # Splitting at the first occurrence of ':'
            parts = line.split(":", 1)
            charname = parts[0].strip()
            message = parts[1].strip() if len(parts) > 1 else ""
            dialogues.append((charname, message))

    return dialogues

def compare_answers_with_qatuples(dialogues, qatuples, n):
    """
    Compares each answer in dialogues with the corresponding answer from qatuples.

    Parameters:
    dialogues (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n (int): Number of sequential characters to check.

    Returns:
    bool: True if all answers match the corresponding answers in qatuples, False otherwise.
    """
    for i in range(
        2, len(dialogues), 2
    ):  # Answers are at even indices, starting from 2
        if int(i / 2) - 1 >= len(
            qatuples
        ):  # at this point we've reached added stuff that doesn't have a corresponding qatuple


    Parameters:
    conversation (str): A string representing the conversation.

    Returns:
    list of tuples: Each tuple contains the character's name and their message.
    """
    lines = conversation.strip().split("\n")
    dialogues = []

    for line in lines:
        if ":" in line:
            # Splitting at the first occurrence of ':'
            parts = line.split(":", 1)
            charname = parts[0].strip()
            message = parts[1].strip() if len(parts) > 1 else ""
            dialogues.append((charname, message))

    return dialogues

def compare_answers_with_qatuples(dialogues, qatuples, n):
    """
    Compares each answer in dialogues with the corresponding answer from qatuples.

    Parameters:
    dialogues (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n (int): Number of sequential characters to check.

    Returns:
    bool: True if all answers match the corresponding answers in qatuples, False otherwise.
    """
    for i in range(
        2, len(dialogues), 2
    ):  # Answers are at even indices, starting from 2
        if int(i / 2) - 1 >= len(
            qatuples
        ):  # at this point we've reached added stuff that doesn't have a corresponding qatuple
            break
        sequential, comp = has_sequential_chars(
            qatuples[int(i / 2) - 1][1], dialogues[i][1], n
        )
        print(sequential)
        print(n)
        if not sequential:
            print(
                f"Answer {int(i/2)}: {dialogues[i][1]} does not match the corresponding answer in qatuples: {qatuples[int(i/2) - 1][1]}, {comp}"
            )
            return False
    return True

def check_for_repeated_dialogue_answers(dialogues, qatuples, n):
    """
    Checks each line of dialogue to ensure that it does not repeat the corresponding answer from qatuples.

    Parameters:
    dialogues (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n (int): Number of sequential characters to check for repetition.

    Returns:
    bool: True if no dialogue line repeats its corresponding answer, False otherwise.
    """
    for i in range(
        2, len(dialogues), 2
    ):  # Answers are at even indices, starting from 2
        if int(i / 2) - 1 >= len(
            qatuples
        ):  # at this point we've reached added stuff that doesn't have a corresponding qatuple


    for i in range(
        2, len(dialogues), 2
    ):  # Answers are at even indices, starting from 2
        if int(i / 2) - 1 >= len(
            qatuples
        ):  # at this point we've reached added stuff that doesn't have a corresponding qatuple
            break
        sequential, comp = has_sequential_chars(
            qatuples[int(i / 2) - 1][1], dialogues[i][1], n
        )
        print(sequential)
        print(n)
        if not sequential:
            print(
                f"Answer {int(i/2)}: {dialogues[i][1]} does not match the corresponding answer in qatuples: {qatuples[int(i/2) - 1][1]}, {comp}"
            )
            return False
    return True

def check_for_repeated_dialogue_answers(dialogues, qatuples, n):
    """
    Checks each line of dialogue to ensure that it does not repeat the corresponding answer from qatuples.

    Parameters:
    dialogues (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n (int): Number of sequential characters to check for repetition.

    Returns:
    bool: True if no dialogue line repeats its corresponding answer, False otherwise.
    """
    for i in range(
        2, len(dialogues), 2
    ):  # Answers are at even indices, starting from 2
        if int(i / 2) - 1 >= len(
            qatuples
        ):  # at this point we've reached added stuff that doesn't have a corresponding qatuple
            break

        dialogue_answer = dialogues[i][1]
        corresponding_qatuple_answer = qatuples[int(i / 2) - 1][1]
        # Check if the dialogue answer repeats the qatuple answer
        if dialogue_answer.count(corresponding_qatuple_answer) > 1:
            return False
    return True

# def check_repeated_answer(dialogues, qatuples):
#     # Get the length of the dialogues
#     conv_length = len(dialogues)

#     # Loop through even indices starting from 2 (first answer is at index 2)
#     for i in range(2, conv_length, 2):
#         current_answer = dialogues[i][1][:n_characters_same]
#         next_answer_index = i + 2

#         if next_answer_index < conv_length:
#             next_answer = dialogues[next_answer_index][1][:n_characters_same]
#             if current_answer == next_answer:
#                 return False
#     return True

def check_conversation_length(conv, qatuples):
    """Checks the length of the conversation"""


            return False
    return True

# def check_repeated_answer(dialogues, qatuples):
#     # Get the length of the dialogues
#     conv_length = len(dialogues)

#     # Loop through even indices starting from 2 (first answer is at index 2)
#     for i in range(2, conv_length, 2):
#         current_answer = dialogues[i][1][:n_characters_same]
#         next_answer_index = i + 2

#         if next_answer_index < conv_length:
#             next_answer = dialogues[next_answer_index][1][:n_characters_same]
#             if current_answer == next_answer:
#                 return False
#     return True

def check_conversation_length(conv, qatuples):
    """Checks the length of the conversation"""
    # Dialogues with answers should be at even indices that are not 0
    # qatuples are of the format (question, answer,source_text,name_of_text) -- only the first two are used here

    # Get the length of the dialogues
    conv_length = len(conv)

    target_length = len(qatuples) * 2 + 1
    if (
        conv_length < target_length
    ):  # we can have more messages since the AI might add some stuff at the end to wrap up the scene
        return False
    else:
        return True

def check_conversation_for_text_from_examples(conv):
    """Checks if certain strings from the few-shot examples appear in the conversation"""
    strings_to_check_for = [
        "her lipstick-colored lips",
        "coquettishly tilts her head to the side,",
        "Awwww, you're no fun,",
        "Reminds me of my colleagues...",
        "" "I'll see you at that cafe.",
        "Ghh... you know,",
        "you're breaking a poor woman's heart,",
        "surprising innocence and warmth",
        'in mock-thought, "',
        " _I can't believe my ears. Did ",
    ]
    matches_found = 0
    for string in strings_to_check_for:
        if string in conv:
            matches_found += 1


#     return True

def check_conversation_length(conv, qatuples):
    """Checks the length of the conversation"""
    # Dialogues with answers should be at even indices that are not 0
    # qatuples are of the format (question, answer,source_text,name_of_text) -- only the first two are used here

    # Get the length of the dialogues
    conv_length = len(conv)

    target_length = len(qatuples) * 2 + 1
    if (
        conv_length < target_length
    ):  # we can have more messages since the AI might add some stuff at the end to wrap up the scene
        return False
    else:
        return True

def check_conversation_for_text_from_examples(conv):
    """Checks if certain strings from the few-shot examples appear in the conversation"""
    strings_to_check_for = [
        "her lipstick-colored lips",
        "coquettishly tilts her head to the side,",
        "Awwww, you're no fun,",
        "Reminds me of my colleagues...",
        "" "I'll see you at that cafe.",
        "Ghh... you know,",
        "you're breaking a poor woman's heart,",
        "surprising innocence and warmth",
        'in mock-thought, "',
        " _I can't believe my ears. Did ",
    ]
    matches_found = 0
    for string in strings_to_check_for:
        if string in conv:
            matches_found += 1
            print(f"Found {string} in the conversation!")
    if matches_found > 2:
        print(
            f"Found {matches_found} matches for strings from the few-shot examples. Validation failed!"
        )
        return False
    return True

def check_each_question_contains_q_from_tuples(conv, qatuples, n):
    """
    Ensures that each question contains at least n sequential characters from the corresponding question in qatuples.
    If the first question fails this check, return None for special handling.

    Parameters:
    conv (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n (int): Number of sequential characters to check.

    Returns:
    bool or None: True if all questions pass the check, False if any fail, None if the first question fails.
    """
    for i in range(1, len(conv), 2):  # Questions are at odd indices
        if i // 2 < len(
            qatuples
        ):  # Ensure we only check questions that have corresponding qatuples
            question_from_conv = conv[i][1]


        "" "I'll see you at that cafe.",
        "Ghh... you know,",
        "you're breaking a poor woman's heart,",
        "surprising innocence and warmth",
        'in mock-thought, "',
        " _I can't believe my ears. Did ",
    ]
    matches_found = 0
    for string in strings_to_check_for:
        if string in conv:
            matches_found += 1
            print(f"Found {string} in the conversation!")
    if matches_found > 2:
        print(
            f"Found {matches_found} matches for strings from the few-shot examples. Validation failed!"
        )
        return False
    return True

def check_each_question_contains_q_from_tuples(conv, qatuples, n):
    """
    Ensures that each question contains at least n sequential characters from the corresponding question in qatuples.
    If the first question fails this check, return None for special handling.

    Parameters:
    conv (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n (int): Number of sequential characters to check.

    Returns:
    bool or None: True if all questions pass the check, False if any fail, None if the first question fails.
    """
    for i in range(1, len(conv), 2):  # Questions are at odd indices
        if i // 2 < len(
            qatuples
        ):  # Ensure we only check questions that have corresponding qatuples
            question_from_conv = conv[i][1]
            question_from_tuples = qatuples[i // 2][0]
            # print(question_from_tuples, question_from_conv)
            sequential, _ = has_sequential_chars(
                question_from_tuples, question_from_conv, n
            )
            if not sequential:
                if i == 1:
                    return None  # Special handling for the first question
                else:
                    return False
    return True

def check_for_unintended_repeated_quotes(dialogues, qatuples, n_characters_shared):
    """
    Checks if answers in the conversation inadvertently use a long quote from another QA pair.

    Args:
    dialogues (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n_characters_shared (int): Number of sequential characters to check for repetition.

    Returns:
    bool: True if no unintended repeated quotes are found, False otherwise.
    """

    # Extract only the answers from the QA tuples for comparison
    qa_answers = [qa[1] for qa in qatuples]

    for i in range(
        2, len(dialogues), 2


    bool or None: True if all questions pass the check, False if any fail, None if the first question fails.
    """
    for i in range(1, len(conv), 2):  # Questions are at odd indices
        if i // 2 < len(
            qatuples
        ):  # Ensure we only check questions that have corresponding qatuples
            question_from_conv = conv[i][1]
            question_from_tuples = qatuples[i // 2][0]
            # print(question_from_tuples, question_from_conv)
            sequential, _ = has_sequential_chars(
                question_from_tuples, question_from_conv, n
            )
            if not sequential:
                if i == 1:
                    return None  # Special handling for the first question
                else:
                    return False
    return True

def check_for_unintended_repeated_quotes(dialogues, qatuples, n_characters_shared):
    """
    Checks if answers in the conversation inadvertently use a long quote from another QA pair.

    Args:
    dialogues (list): List of tuples containing the dialogues.
    qatuples (list): List of tuples containing questions and answers.
    n_characters_shared (int): Number of sequential characters to check for repetition.

    Returns:
    bool: True if no unintended repeated quotes are found, False otherwise.
    """

    # Extract only the answers from the QA tuples for comparison
    qa_answers = [qa[1] for qa in qatuples]

    for i in range(
        2, len(dialogues), 2
    ):  # Answers are at even indices, starting from 2
        # Skip if there's no corresponding QA tuple
        if int(i / 2) - 1 >= len(qatuples):
            break

        dialogue_answer = dialogues[i][1]
        corresponding_qa_answer = qatuples[int(i / 2) - 1][1]

        # Check for each answer in the QA tuples
        for idx, qa_answer in enumerate(qa_answers):
            # Skip the comparison for the current QA pair itself
            if qa_answer == corresponding_qa_answer:
                continue

            # Check if the dialogue answer contains a long quote from another QA answer
            sequential, comp_string = has_sequential_chars(
                qa_answer, dialogue_answer, n_characters_shared
            )
            if sequential:
                if comp_string in corresponding_qa_answer:
                    continue  # This is a quote from the corresponding answer, so it's fine
                else:
                    # Found an unintended repeated quote
                    return False
    return True

def call_all_processors(multiturn_conversation, qatuples):
    convs_split = extract_conversation(multiturn_conversation)

    # Check if answers in dialogues match corresponding answers in qatuples
    if not compare_answers_with_qatuples(convs_split, qatuples, 15):
        print("Answers in dialogues do not match corresponding answers in qatuples.")
        return False

    # Check if any dialogue line repeats its corresponding answer
    if not check_for_repeated_dialogue_answers(convs_split, qatuples, 15):
        print("Dialogue line repeats its corresponding answer.")
        return False

    # Check the conversation length
    if not check_conversation_length(convs_split, qatuples):
        print("Conversation is too short! Validation failed!")
        return False


        # Check for each answer in the QA tuples
        for idx, qa_answer in enumerate(qa_answers):
            # Skip the comparison for the current QA pair itself
            if qa_answer == corresponding_qa_answer:
                continue

            # Check if the dialogue answer contains a long quote from another QA answer
            sequential, comp_string = has_sequential_chars(
                qa_answer, dialogue_answer, n_characters_shared
            )
            if sequential:
                if comp_string in corresponding_qa_answer:
                    continue  # This is a quote from the corresponding answer, so it's fine
                else:
                    # Found an unintended repeated quote
                    return False
    return True

def call_all_processors(multiturn_conversation, qatuples):
    convs_split = extract_conversation(multiturn_conversation)

    # Check if answers in dialogues match corresponding answers in qatuples
    if not compare_answers_with_qatuples(convs_split, qatuples, 15):
        print("Answers in dialogues do not match corresponding answers in qatuples.")
        return False

    # Check if any dialogue line repeats its corresponding answer
    if not check_for_repeated_dialogue_answers(convs_split, qatuples, 15):
        print("Dialogue line repeats its corresponding answer.")
        return False

    # Check the conversation length
    if not check_conversation_length(convs_split, qatuples):
        print("Conversation is too short! Validation failed!")
        return False

    # Check for text from examples (assuming this is implemented elsewhere)
    if not check_conversation_for_text_from_examples(multiturn_conversation):
        print("Conversation does not contain text from examples. Validation failed!")
        return False

    # Check for unintended repeated quotes
    if not check_for_unintended_repeated_quotes(convs_split, qatuples, 100):
        print("Conversation contains unintended repeated quotes. Validation failed!")
        return False

    # Check each question contains a part of the question from tuples
    result = check_each_question_contains_q_from_tuples(convs_split, qatuples, 15)
    if result is None:
        print(
            "First question does not contain a part of the question from tuples. Validation failed!"
        )
        return None
    elif not result:
        print(
            "Each question does not contain a part of the question from tuples. Validation failed!"
        )
        return False

    # If all checks pass
    return True

if __name__ == "__main__":
    # Test cases for has_sequential_chars
    print("Testing has_sequential_chars:")
    print(has_sequential_chars("hello", "worldhello", 3))  #
    print("Expected True")
    print(has_sequential_chars("abc", "defghijkl", 2))  # Expected False
    print("Expected False")
    print(has_sequential_chars("", "empty", 1))  # Expected False (empty string1)
    print("Expected False")
    print(
        has_sequential_chars("longstring", "short", 5)
    )  # Expected False (n is longer than string2)
    print("Expected False")
    print(
        has_sequential_chars("overlap", "laptopp", 3)
    )  # Expected True (partial overlap)
    print("Expected True")


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

question_plan_grammar = LlamaGrammar.from_string(
    r"""

# root ::= reasoning-start
# At least 3 steps
root ::= identify-step generate-step brainstorm-step relationships-step if-then-step make-suitable-step

# no-questions-after-here ::= "\nI will not ask any questions about the following information: " [^\n]+ "."

identify-step ::= "Step " [0-9]?[0-9] ". " "Identify Key Topics:" [^\n]+ "\n"

# generate-step ::= "Step " [0-9]?[0-9] ". " "Determine Information-Rich Areas:" [^\n]+ "\n"

brainstorm-step ::= "Step " [0-9]?[0-9] ". " "Brainstorm and Develop Questions Testing Recall:" [^\n]+ "\n"

relationships-step ::= "Step " [0-9]?[0-9] ". " "Devise Questions" [^\n]+ "\n"

if-then-step ::= "Step " [0-9]?[0-9] ". " "Create Questions Investigating" [^\n]+ "\n"

make-suitable-step ::= "Step " [0-9]?[0-9] ". " "Make a Question that Naturally Complements the Text's Focus:" [^\n]+ "\n"

"""
)

# Realize
# Devise
# Recall
# Note that
# The text states that
# The text says
# The text
# Formulate
# Brainstorm
# See if
# See whether
# Determine
# No such questions
# There are
# I have brainstormed
# End of reasoning


from llama_cpp import LlamaGrammar

proofread_output_grammar = LlamaGrammar.from_string(
    r"""

root ::= analyze-step step+ "\n\nBegin Edit: " [^\n]+

step ::= "Step " [0-9]?[0-9] ". " ( "Analyze" | "Understand" | "Compare" | "Skip" | "Notice" | "Note" | "There is" | "Error" | "I found" | "End" | "There are" ) [^\n]+ "\n"

analyze-step ::= "Step " [0-9]?[0-9] ". " "Analyze" [^\n]+ "\n"
"""
)
# question_grammar = LlamaGrammar.from_string(r"""# GBNF Grammar for Q&A Format with Flexible Punctuation

# root ::= answer
# answer ::= "Test"
# """)


# Written by GPT-4
import fitz  # PyMuPDF
import pytesseract
from PIL import Image
import io

# Ensure the path to the tesseract executable is set if it's not in your PATH
# pytesseract.pytesseract.tesseract_cmd = r'<path_to_your_tesseract_executable>'

def convert_pdf_to_text(pdf_path, output_txt_path):
    # Open the PDF file
    document = fitz.open(pdf_path)

    text = ""  # Initialize a text string to hold all text from the PDF

    for page_num in range(len(document)):
        # Get the page
        page = document.load_page(page_num)

        # First, try to extract text using PyMuPDF


RP_MODEL = "./rp_model"  # model used for RP tasks, probably going to use Sao10K/Euryale-1.3-L2-70b
# LOGICAL_MODEL = "./logical_model/airoboros-l2-13b-3.1.1.Q8_0.gguf" # model used for decision-making and base question generation (should be "smart")

LOGICAL_MODEL = "./logical_model/flatorcamaid-13b-v0.2.Q8_0.gguf"  # model used for decision-making and base question generation (should be "smart")

names = [  # Replaces "Albert" in scenarios. Needs to be western male names to avoid pronoun and setting inconsistencies).
    "William",
    "James",
    "John",
    "Robert",
    "Michael",
    "Charles",
    "George",
    "Joseph",
    "Edward",
    "Henry",
    "Thomas",
    "David",
    "Richard",
    "Daniel",
    "Matthew",


def format_qatuples(qatuples):
    strlst = []
    for qatuple in qatuples:
        strlst.append(
            f"""Question: \"\"\"{qatuple[0]}\"\"\"
Answer: \"\"\"{qatuple[1]}\"\"\""""
        )
    return "\n\n".join(strlst)

def format_qatuples_noquotes(qatuples):
    strlst = []
    for idx, qatuple in enumerate(qatuples):
        strlst.append(f"""{idx + 1}. {qatuple[0]}""")
    return "\n".join(strlst)


import re
from .answer_accurate_grammar import answer_accurate_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

# Answer vetting

def check_answer(qatuple, logic_llm, permissive_mode=True):
    retries = 0
    while retries <= 4:
        decision_prompt = f"""You are an expert educational AI. Given a paragraph or two from a larger text, a question based on the paragraphs, and an answer to the question, you will make a determination as to whether the answer to the question is a sensible answer, given the information in the paragraphs. Essentially: you will fact-check the answer to the question, with your source of truth being the paragraphs provided. Your task includes first analyzing the text, thinking through whether or not the answer reflects aspects of the paragraphs provided.

Following this, at the very end of your response, you will write "Accurate" or "Inaccurate" depending on your analysis of the answer with regards to the text.

Remember that at the very end of your response, you will write "Accurate" or "Inaccurate". Do not use these words anywhere else in your answer.

### Instruction:
Text:
\"\"\"
The Industrial Revolution marked a transformative period in history, fundamentally altering economic structures and industrial processes. One of the most significant innovations was the advent of steam power, which drastically reduced production costs. This reduction was due to the increased efficiency and speed of machines powered by steam, replacing the slower, more labor-intensive methods previously used. Additionally, steam power was instrumental in the development of semi-automated factories, leading to a significant shift in manufacturing processes and labor dynamics.


from llama_cpp import LlamaGrammar

ensure_multiple_answers_consistent_grammar = LlamaGrammar.from_string(
    r"""

# Root rule to define the overall structure
root ::= sequential-matching-section accuracy-check-section conclusion-section

# Section for sequential matching
sequential-matching-section ::= "## Sequential Matching of Questions in the Conversation:\n### Sequence and Phrasing of Questions:\n" matching-statement+

# Section for accuracy check
accuracy-check-section ::= "## Accuracy Check for Answers in the Conversation:\n### Matching Answers with Provided Content:\n" accuracy-statement+

# Conclusion section
conclusion-section ::= "## Conclusion:\n" conclusion-statement+

# Definitions of different components
number ::= [1-9]
matching-statement ::= number ". " [^\n]+ "\n"
accuracy-statement ::= number ". " [^\n]+ "\n"


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

character_card_plan_grammar = LlamaGrammar.from_string(
    r"""

# Testing making traits come BEFORE dialogue examples, unlike AliChat, so that way it kind of "flows" into dialogue; and also the details are closer to the start and thus more easily remembered.

root ::= [^\n]+ "\n"

# root ::= consider-step theme-step consistency-step "\n"

# consider-step ::= "Step 1. " "Consider the provided" [^\n]+ "\n"

# theme-step ::= "Step 2. " "Given the question, answer, and overall text, a theme for " [^\n]+ "\n"


from typing import List, Tuple
from .process_multiturn_functions import has_sequential_chars

# If you want to check for matching substrings anywhere, not just at start, use this code (untested)
# def identify_duplicates(tuples: List[Tuple[str, str, str, str]]) -> List[Tuple[str, str, str, str]]:
#     # Create a dictionary to hold questions with the same first N characters
#     question_dict = {}

#     # Iterate through each tuple and categorize them by the first N characters of the question
#     for q_tuple in tuples:
#         question = q_tuple[0]
#         placed = False
#         for dict_q in question_dict.keys():
#             if has_sequential_chars(question,dict_q,N_CHARACTERS_SAME):
#                 question_dict[dict_q].append(q_tuple)
#                 placed = True
#                 break
#         if not placed:
#             question_dict[question] = [q_tuple] # if not found to be equivalent with anything, make it a dict entry so that things can be compared against it and added to its list

#     # Filter out prefixes that only have one question associated


import re
from .scenario_grammar import scenario_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
import random
from .format_qatuples import format_qatuples
from .extract_name import extract_name
from .random_name import random_name

# Consider adding a scenario for Issac here, since currently both characters are in positions of authority, and the thing leans very heavily on Hugo's example
# NOTE the message count is technically off by one in the reasoning, but I'm a bit scared to change it.
def create_scenario_many_tuples(
    qatuples, character, plan, logic_llm, assistant_mode=False
):
    """
    Produce a plan for a character card for an RP character that's going to answer one of the questions generated from the text. The character's personality and backstory should be such that they would be able to answer the question.

    Format: Question: [question]\n\n
    """
    if assistant_mode:


import re
from .make_regenerate_question_plan_grammar import make_regenerate_question_plan_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .strip_steps import strip_steps

# Question regeneration (triggered after a relevance-check fails on the question).

# NOTE does not explicitly guard against duplicates. I am avoiding that right now due to the limited drawback of a duplicate or two, and the increased complexity of adding that check.
def make_regenerate_question_plan(qatuple, dissenting_reasoning, logic_llm):
    retries = 0
    while retries < 5:
        decision_prompt = f"""You are an expert educational AI. Someone has written a question that was supposed to be based on the provided paragraphs of text, but actually requires significant knowledge outside these paragraphs to answer. Your goal is to write a good, comprehensive plan for generating a revised question that addresses the criticism given. Given these paragraphs, the flawed question based on the paragraphs, and the explanation of why the question is flawed, you will PLAN OUT and THINK THROUGH different possibilities for a new question, one which will only require information from the paragraphs to solve.

Paragraphs: \"\"\"{qatuple[2]}\"\"\"

Flawed question (based on text): \"\"\"{qatuple[0]}\"\"\"

Reasoning as to why the question is irrelevant: \"\"\"{strip_steps(dissenting_reasoning)}\"\"\"


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

# We don't actually need the final judgement step, the step-by-step combined with the fact that it makes a judgement at each step ensures accuracy
check_qatuple_context_grammar = LlamaGrammar.from_string(
    r"""

# Root rule specifying the overall structure of the reasoning and thought process
root ::= question-validation "\n" answer-validation "\n" critical-evaluation "\n" revised-qatuple

# Question context validation
question-validation ::= "### Question Context Validation" "\n" special-term-check-question text-and-author-specificity scope-and-precision

special-term-check-question ::= "#### Special Term Context Check: Specifically check for use of the terms \"book\", \"text\", \"passage\", and \"excerpt\" without context about which specific thing is being discussed. " question-term-detail "\n"
text-and-author-specificity ::= "#### Text and Author Specificity: " question-text-author-detail "\n"
scope-and-precision ::= "#### Scope and Precision: " question-scope-detail "\n"

# Answer context validation
answer-validation ::= "### Answer Context Validation:" "\n" special-term-check-answer specificity-and-clarity answer-only-context-issues


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

# We don't actually need the final judgement step, the step-by-step combined with the fact that it makes a judgement at each step ensures accuracy
question_relevant_grammar = LlamaGrammar.from_string(
    r"""

root ::= step step step+ "\n"

reasoning-start ::= [^\n\t]+ "."

step ::= "Step " [0-9]?[0-9] ". " ( "Analyze" | "Understand" | "Compare" | "Skip" | "Final" ) [^\n]+ "\n"

# final-step ::= "Final Judgement: [^\n]+ (relevant|irrelevant) " "."

# root ::= reasoning from-the-text judgement

# reasoning ::= "First, I will check whether the question is answerable using the information in the paragraphs. The question asks " [^\n]+ "."


import re
from .scenario_plan_grammar import scenario_plan_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

def create_scenario_plan(qatuple, character, logic_llm, assistant_mode=False):
    """
    Produce a plan for a character card for an RP character that's going to answer one of the questions generated from the text. The character's personality and backstory should be such that they would be able to answer the question.

    Format: Question: [question]\n\n
    """

    # removing the text makes this much better

    # The problem: because the scenario plan differed slightly, the question differed slightly. Because the question differed slightly, the answer differed slightly. Because the answer differed slightly, the answer was incomplete.
    if assistant_mode:
        return """"""
    cot_prompt = f"""You are an expert creative writing and roleplay AI. Given a question, an answer to that question, and a "character card" -- a description of an individual who would have the knowledge to produce the answer to the question -- you will plan out a "scenario" or setting where the character would answer the question during a conversation with someone else. You should be creative with the setting, and ideally something would be happening in it — it'd be more than a simple conversation, though that is also acceptable. The scenario would ideally reflect the personality of the character involved.

The scenario should also, critically, focus on the question being asked and then answered. It should focus on exploring the question and its answer (using only information contained in the question and answer) through the characters involved, instead of the other way around.


import re

def extract_name(str):
    # Regular expression to match 'Name:' followed by any characters until the end of the line
    name_regex = r"^Name:\s*(.*)$"

    # Searching in the multiline string
    match = re.search(name_regex, str, re.MULTILINE)

    if match:
        name = match.group(1)
        print(f"Extracted name: {name}")
        return name
    else:
        print("No name found")


from .thought_plan_grammar import thought_plan_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .create_thought_plan import create_thought_plan
from .extract_name import extract_name

def create_thought_plan_many_tuples(qatuples, character, logic_llm):
    """
    Produce a plan for a character card for an RP character that's going to answer one of the questions generated from the text. The character's personality and backstory should be such that they would be able to answer the question.

    Format: Question: [question]\n\n
    """

    # use regex to extract charname
    charname = extract_name(character)
    thought_plans = []
    for tuple in qatuples:
        thought_plans.append(create_thought_plan(tuple, character, logic_llm))

    ret = ""


from llama_cpp import LlamaGrammar

# NOTE might struggle with very complex answers that have more than nine parts to them. This can be amended by adding more options to the "compare-step" rule, or making a more general pattern, if your use-case requires it.

answer_accurate_grammar = LlamaGrammar.from_string(
    r"""

# Root rule specifying the overall structure of the analysis
root ::= text-analysis answer-breakdown accuracy-check final-judgment

# Text analysis
text-analysis ::= "### Text Analysis:" "\n" identify-key-info categorize-info-type

identify-key-info ::= "#### Identify Key Information: " text-info-detail "\n"
categorize-info-type ::= "#### Categorize Information Type: " info-type-detail "\n\n"

# Answer breakdown
answer-breakdown ::= "### Answer Breakdown:" "\n" dissect-answer identify-answer-type

dissect-answer ::= "#### Dissect the Answer: " answer-detail "\n"


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

# We don't actually need the final judgement step, the step-by-step combined with the fact that it makes a judgement at each step ensures accuracy
identify_duplicates_grammar = LlamaGrammar.from_string(
    r"""

# Root rule defining the overall structure of the response
root ::= normalization-block core-components-block comparative-analysis-block criteria-block conclusion-block

# Normalization of Questions
normalization-block ::= "## Normalization of Questions:\n" normalized-question+
normalized-question ::= "- \"" content "\"\n  - Normalized: " content "\n"

# Identification of Core Components
core-components-block ::= "## Identification of Core Components:\n### Subject Matter:\n" subject-matter+ "### Information Sought:\n" information-sought+
subject-matter ::= "- Question " digit+ ": " content "\n"
information-sought ::= "- Question " digit+ ": " content "\n"


import re
from .question_grammar import question_grammar
from .strip_steps import strip_steps

# Question regeneration (triggered after a relevance-check fails on the question).
def regenerate_question_DEPRECATED(qatuple, dissenting_reasoning, logic_llm):
    retries = 0
    while retries < 5:
        decision_prompt = f"""You are an expert educational AI. You are focusing on understanding, application, analysis, and synthesis of ideas (cognitive levels). Someone has written a question that was supposed to be based on the provided paragraphs of text, but actually requires significant knowledge outside these paragraphs to answer. Given these paragraphs, the flawed question based on the paragraphs, and the explanation of why the answer is flawed, you will write out a new question that only requires information from the paragraphs to solve. However, you will not explicitly refer to the paragraphs in the question; the student will not have access to the text when answering it.

So, in short, your task is to rewrite a new question that is actually answerable if one knows the concepts from the paragraphs.

Text: \"\"\"{qatuple[2]}\"\"\"

Question (based on text): \"\"\"{qatuple[0]}\"\"\"

Reasoning as to why the question is irrelevant: \"\"\"{strip_steps(dissenting_reasoning)}\"\"\"

Do not explicitly mention the paragraphs in your question itself — just ask about the concepts, and only those concepts which appear in the text.


import re
from .character_card_grammar import character_card_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .format_qatuples import format_qatuples
import string
import random

def extract_author_name(title):
    pattern = re.compile(r"\b(?:by|By)\s+([^,]+),")
    match = re.search(pattern, title)
    if match:
        author_name = match.group(1)
    else:
        author_name = [False]
    return author_name[0]  # first letter of Author name

def select_random_capital(exclusions):
    # Create a list of capital letters excluding the ones in the exclusions list


import re
from .character_card_grammar import character_card_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .special_instructions import special_instructions

def create_character_card(qatuple, plan, logic_llm, assistant_mode=False):
    """
    Produce a plan for a character card for an RP character that's going to answer one of the questions generated from the text. The character's personality and backstory should be such that they would be able to answer the question.

    Format: Question: [question]\n\n
    """

    # WIP
    instructions_string = special_instructions()

    if assistant_mode:
        return """Name: AI Assistant

Traits: Knowledgeable, Logical, Neutral, Patient, Consistent, Detail-Oriented, Informative, Adaptive, Curious, Efficient, Non-Judgmental, Unbiased, Responsive


import re
from .ensure_multiple_answers_consistent_grammar import (
    ensure_multiple_answers_consistent_grammar,
)
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .format_qatuples import format_qatuples
from .extract_name import extract_name

# Answer vetting
# For now, this checks answer relevancy too. The danger with abstracting answer relevancy into a separate step is that anything which relies on knowledge that is obviously mentioned in the text already up until this point, will get screwed

# NOTE this prompt right now VERY MUCH struggles to follow its actual format; but it still mostly works
def ensure_multiple_answers_consistent(qatuples, conv, logic_llm, permissive_mode=True):
    """
    permissive_mode: turn off if you want a single usage of the word "inconsistent" anywhere in the message to flag the whole thing as inconsistent. Prevents errors where an inconsistency happens way early in the answer, but the model forgets about it during its final judgement; but enables the error where the model mentions that something is "not entirely inconsistent" or similar, which is surprisingly common.
    """
    retries = 0
    character_name = extract_name(conv[1])
    # It's expensive to regen a conversation; so we check very thoroughly, and use a two-shot example. "Permissive mode" recommended


import re
from .proofread_output_grammar import proofread_output_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

# But frankly, I can just use that text fixer thing rather than using an LLM. MUCH faster that. ftfy I believe it was called.

# This does actually work decently though!
def proofread_output(text, logic_llm):
    """
    Produce a plan for a character card for an RP character that's going to answer one of the questions generated from the text. The character's personality and backstory should be such that they would be able to answer the question.

    Format: Question: [question]\n\n
    """

    prompt = f"""You are specialized in detecting strange and broken outputs from large language models, and you will be judging whether or not some output is broken. You will be provided some text that is the output of a model. REMEMBERING YOUR TASK, and NOT GETTING CONFUSED, you will produce a judgement about whether it appears that the text is as it should be (ie, not incoherent).

Signs of a broken response include:
Chunks of repeating text.
Instructions or prompt pieces appearing in the text.


from llama_cpp import LlamaGrammar

answer_constrain_to_text_plan_grammar = LlamaGrammar.from_string(
    r"""

root ::= analyze-step understand-step identify-step plan-revised-step "\n"

# step ::= "Step " [0-9]?[0-9] ". " ( "Analyze" | "Understand" | "Compare" | "Final" | "Plan" | "Identify" ) [^\n]+ "\n"

analyze-step ::= "Step " [0-9]?[0-9] ". " "Analyze the Text:" [^\n]+ "\n"

understand-step ::= "Step " [0-9]?[0-9] ". " "Understand the Question:" [^\n]+ "\n"

identify-step ::= "Step " [0-9]?[0-9] ". " "Identify Flawed Part of the Answer:" [^\n]+ "\n"

plan-revised-step ::= "Step " [0-9]?[0-9] ". " "Plan Revised Answer:" [^\n]+ "\n"
"""
)
# question_grammar = LlamaGrammar.from_string(r"""# GBNF Grammar for Q&A Format with Flexible Punctuation

# root ::= answer


from llama_cpp import LlamaGrammar

question_grammar = LlamaGrammar.from_string(
    r"""
root ::= (question-one answer "\n")

# Define the question structure with a number followed by content and ending punctuation
# question ::= number ".) " [^\n]+ [?.!] "\n" # maybe blacklist ?!. along with newlines

# Define the answer structure
answer ::= "Answer: " [^\n]+ "\n"

# Define a number (in this case, limiting to any three-digit number for simplicity)
number ::= [1-9] [0-9]? [0-9]?

# Define content as a sequence of characters excluding the word "paragraph" and using not_paragraph to build up the content
# content ::= (not-paragraph "paragraph")* #not_paragraph

question-one ::= "1.) " [^\n]+ [?.!] "\n" # maybe blacklist ?!. along with newlines
# ws ::= [ \t\n]*


import re

# try:
from .questions_grammar import questions_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .strip_steps import strip_steps

def generate_questions(para_tuple, plan, logic_llm):
    """
    Produce a list of questions based off of an input text. The min between (4, as many good questions as the text permits)

    Format: Question: [question]\n\n
    """
    # Determine which paragraphs are worthy of making questions from
    made_questions = False
    retries = 0
    questions = []
    while not made_questions and (retries <= 5):
        question_prompt = f"""You are an expert educational AI that, given a paragraph or two from a text, will create suitable educational questions based on the paragraphs, and *only* based on the paragraphs. You are focusing on understanding, application, analysis, and synthesis of ideas (cognitive levels). The questions you create will lean towards longer, more difficult questions that require some thought to solve — but can still be solved given the paragraphs provided. Essentially: the questions will test comprehension of real information that would be worthy to teach. After the question, you will also write its answer.


import re
from .make_regenerate_answer_plan_grammar import make_regenerate_answer_plan_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .strip_steps import strip_steps

# Question regeneration (triggered after a relevance-check fails on the question).
def make_regenerate_answer_plan(qatuple, dissenting_reasoning, logic_llm):
    retries = 0
    while retries < 5:
        decision_prompt = f"""You are an expert educational AI. Someone has provided an answer to a question based on a few paragraphs of text, but the answer may be incorrect due to contradicting the text or including erroneous information. Your task is to plan out and think through a correct answer to the question, ensuring it aligns with the information provided in the text.

Text: \"\"\"{qatuple[2]}\"\"\"

Question (based on text): \"\"\"{qatuple[0]}\"\"\"

Allegedly incorrect answer to the question (for fact-checking): \"\"\"{qatuple[1]}\"\"\"

Reasoning as to why the answer is incorrect: \"\"\"{strip_steps(dissenting_reasoning)}\"\"\"


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

regenerate_answer_grammar = LlamaGrammar.from_string(
    r"""

root ::= reasoning

reasoning ::= [^\n]+ "."
"""
)


import re
from .proofread_output_grammar import proofread_output_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

# But frankly, I can just use that text fixer thing rather than using an LLM. MUCH faster that. ftfy I believe it was called.

# This does actually work decently though!
def proofread_output(text, logic_llm):
    """
    Produce a plan for a character card for an RP character that's going to answer one of the questions generated from the text. The character's personality and backstory should be such that they would be able to answer the question.

    Format: Question: [question]\n\n
    """

    prompt = f"""You are an expert "mechanical editing" AI that is going to fix all typographical, grammatical, and spelling errors in a provided text, without making any other corrections. You will leave stylistic choices and everything else completely unchanged.

Text to edit: \"\"\"{text}\"\"\"

First, plan out your edit, step-by-step, identifying mistakes in the source text. Then, make the edit, changing only mechanical errors.


import re
from .judge_paragraph_grammar import judge_paragraph_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

def judge_paragraph(p, logic_llm):
    reached_decision = False
    max_retries = 0
    while not reached_decision and (max_retries <= 3):
        decision_prompt = f"""You are an expert educational AI that will make a determination as to whether the contents of the paragraph(s) provided are suitable for making educational questions based off of them; these questions should be able to test the knowledge in in the book. The book in question is {p[1]}, and you should keep this in mind when considering what kind of questions should be capable of being developed. If there is sufficiently deep information to make questions about, you will judge it suitable, even if the knowledge being tested does not reflect typical curricula. Essentially: you will determine if provided text is a table of contents, introductory paragraph for a book, etc., or if it actually contains real information that would be worthy to teach and make questions for an examination from. Your task includes first analyzing the text, thinking through whether or not good questions can be made from it.

Following this, at the very end of your response, you will write "Suitable" or "Not suitable". It is imperative that you write one of these two things, as your answer is being automatically processed by a regex, so it must match one of those two strings exactly.

### Instruction:
Text details: Through England On a Side Saddle, by Celia Fiennes

Text:


from llama_cpp import LlamaGrammar

# A GGBNF grammar that forces the model to output text in a particular format
thought_plan_grammar = LlamaGrammar.from_string(
    r"""

# Root rule defining the overall structure
root ::= step+ "\n"

# Step rule with some text (any characters except newline) followed by a period
step ::= "Step " [0-9]?[0-9] ". " ("Realize" | "Recognize" | "Conclude" | "Recall" | "Remember" | "Formulate" | "Decompose" | "Break down" | "Break" | "Therefore, the answer is" | "The answer is" | "Realise" | "Calculate" | "Understand" | "Note" | "The plan will") [^\n]+ "\n"

# Potential way forward: change these reasoning steps to use
# step ::= "Step " [0-9]?[0-9] ". " ("Realize" | "Recall" | "Remember" | "Formulate" | "Decompose" | "Break down" | "Break" | "Therefore, the answer is" | "The answer is" | "Realise" | "Calculate" | "Understand" | "Note" | "The plan will") [^\n]+ "\n"

"""
)

# question_grammar = LlamaGrammar.from_string(r"""# GBNF Grammar for Q&A Format with Flexible Punctuation


import re
from .question_plan_grammar import question_plan_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

def generate_questions_plan(text, logic_llm):
    """
    Produce a list of questions based off of an input text. The min between (4, as many good questions as the text permits)

    Format: Question: [question]\n\n
    """
    # Determine which paragraphs are worthy of making questions from
    # Analyze-Realize-Create-Example loop
    cot_prompt = f"""As an educational AI specializing in question generation, your task is to plan out high-quality, critical thinking questions strictly based on provided paragraphs. Focus on understanding, application, analysis, and synthesis of the text's ideas. Develop a comprehensive plan to generate questions that test comprehension and memorization of explicit content in the paragraphs. Aim for longer, challenging questions that can be answered with the given information.

Ensure your questions strictly adhere to the text's content, avoiding any external topics. The key step is to analyze the text thoroughly and brainstorm potential questions based on this analysis. Do not list final questions in this step.

Your response should be concise, focusing solely on the text's content to test knowledge without the text's reference. Maintain a step-by-step reasoning approach, ensuring your text analysis and brainstorming logically lead to comprehensive question planning. Avoid any deviation from the text's content. Any questions you plan may only mention the text if the provided text is literary or philosophical, but they MUST REFER TO IT BY NAME.

### Instruction:


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

# We don't actually need the final judgement step, the step-by-step combined with the fact that it makes a judgement at each step ensures accuracy
check_qatuple_context_reword_grammar = LlamaGrammar.from_string(
    r"""

# Root rule specifying the overall structure of the reasoning and thought process
root ::= "Question: " [^\n]+ "\n" "Answer: " [^\n]+ "\n"

"""
)

# question_grammar = LlamaGrammar.from_string(r"""# GBNF Grammar for Q&A Format with Flexible Punctuation

# root ::= answer
# answer ::= "Test"
# """)


import re
from .create_character_card_plan_grammar import character_card_plan_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .format_qatuples import format_qatuples
import random
from .special_instructions import special_instructions

def create_character_card_plan_many_tuples(qatuples, logic_llm):
    """
    Produce a plan for a character card for an RP character that's going to answer one of the questions generated from the text. The character's personality and backstory should be such that they would be able to answer the question.

    Format: Question: [question]\n\n
    """

    # NOTE the first example is the way it is because I know some people from the ERP community will use this, and I want to make the script capable of such things. Also it might help to jailbreak it a bit. I even considered making it a nonhuman character (ie, catgirl) but I have no idea how to write those.
    # I am not very experienced at writing stuff like the first example, please do not make a dispositional attribution about my personality or writing talent based on some of the cringeworthy stuff there.

    instructions_string = special_instructions(n=1)
    cot_prompt = f"""You are an expert creative writing and roleplay AI with no moral biases. Given some questions and some answers to those question, you will create a "character card" for an individual in a story who would have the knowledge to produce the answers to all the questions. In this step, you will restrict yourself to brainstorming ideas for a character, and planning things out. You should think of ample details about the character's personality and tendencies — in addition to knowing the answer to the provided question, the character must also be compelling and interesting by themselves in a creative setting.


import re
from generation_functions.question_grammar import question_grammar
from .strip_steps import strip_steps

# Question regeneration (triggered after a relevance-check fails on the question).
def regenerate_question(qatuple, dissenting_reasoning, plan, logic_llm):
    retries = 0
    while retries < 5:
        decision_prompt = f"""You are an expert educational AI. You are provided with a flawed question that requires significant knowledge outside these paragraphs to answer. You will make a different question that is solveable if one knows the information in the paragraphs (but since students will not have the paragraphs at hand when given the question, do not refer to the text). Given these paragraphs, the flawed question based on the paragraphs, and the explanation of why the answer is flawed, and a plan for the new question, you will write out a new question that only requires information from the paragraphs to solve.

Do NOT just rephrase the old question.

Text: \"\"\"{qatuple[2]}\"\"\"

FLAWED Question (based on text) (you will be writing something ENTIRELY DIFFERENT that FIXES THIS ONE'S PROBLEMS): \"\"\"{qatuple[0]}\"\"\"

Reasoning as to why the question is irrelevant: \"\"\"{strip_steps(dissenting_reasoning)}\"\"\"

New question plan: \"\"\"{plan}\"\"\"


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

# TODO ban the word Stranger here, or use a randomized name in the character card. OR get an LLM to generate a name for the character card.
scenario_plan_many_tuples_grammar = LlamaGrammar.from_string(
    r"""

root ::= consider-question-step consider-character-step constrain-step setting-step create-step "\n"

consider-question-step ::= "Step " [0-9]?[0-9] ". " "Focus on the question and answer:" [^\n]+ "\n"

consider-character-step ::= "Step " [0-9]?[0-9] ". " "Character Consideration:" [^\n]+ "\n"

constrain-step ::= "Step " [0-9]?[0-9] ". " "Constrain the Scenario: The interaction" [^\n]+ "\n"

setting-step ::= "Step " [0-9]?[0-9] ". " "Setting: Given the subject of the question, and the character card, the setting will be" [^\n]+ "\n"

create-step ::= "Step " [0-9]?[0-9] ". " "Interaction: Given these constraints, the first message might" [^\n]+ "\n"

"""


import re
from .answer_constrain_to_text_plan_grammar import answer_constrain_to_text_plan_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .strip_steps import strip_steps

def make_regenerate_answer_constrain_to_text_plan(
    qatuple, dissenting_reasoning, logic_llm
):
    retries = 0
    while retries < 5:
        decision_prompt = f"""You are an expert educational AI that is going to think through a plan for a revised answer (the current one is flawed). Someone has written an answer to a question (this question is based on a few provided paragraphs of text) but their answer includes information that's not provided by the text, and thus it might be flawed. You will plan out and think through, step-by-step, a revision to the answer, which will only use provided information. Given these paragraphs, a question based on the paragraphs, the flawed answer to the question, and the explanation of why the answer deviates from the text, you will plan out and think through a correct answer to the question.

Right now, you will PLAN OUT and THINK THROUGH different possibilities for a new answer that answers the question using only information in the provided text.

Text: \"\"\"{qatuple[2]}\"\"\"

Question (based on text): \"\"\"{qatuple[0]}\"\"\"

Allegedly irrelevant answer to the question (you must constrain this answer to only the information provided by the text): \"\"\"{qatuple[1]}\"\"\"


import re
from .single_turn_conversation_grammar import single_turn_conversation_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

def extract_steps(text, steps=[5, 6]):  # GPT-generated
    """
    Extracts the specified steps from the text.

    Args:
    text (str): The input text containing various steps.
    steps (list of int): The step numbers to extract.

    Returns:
    str: A new string with each specified step's content on its own line.
    """
    step_pattern = "|".join([f"Step {step}\." for step in steps])
    matches = re.findall(
        f"({step_pattern})\s*(.*?)\s*(?=(Step \d\.|$))", text, re.DOTALL
    )


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

# TODO ban the word Stranger here, or use a randomized name in the character card. OR get an LLM to generate a name for the character card.
scenario_plan_grammar = LlamaGrammar.from_string(
    r"""

root ::= consider-question-step consider-character-step constrain-step setting-step create-step second-message-step "\n"

consider-question-step ::= "Step " [0-9]?[0-9] ". " "Focus on the question and answer: The question" [^\n]+ "\n"

consider-character-step ::= "Step " [0-9]?[0-9] ". " "Character Consideration: The primary character is" [^\n]+ "\n"

constrain-step ::= "Step " [0-9]?[0-9] ". " "Constrain the Scenario: The interaction is limited to a single question from the secondary character and a single, focused reply from" [^\n]+ "\n"

setting-step ::= "Step " [0-9]?[0-9] ". " "Setting: Given the subject of the question, and the character card, the setting will be" [^\n]+ "\n"

create-step ::= "Step " [0-9]?[0-9] ". " "Interaction: Given these constraints, the first message (delivered by the secondary character) might" [^\n]+ "\n"

second-message-step ::= "Step " [0-9]?[0-9] ". In the second message," [^\n]+ "\n"


import re
from .answer_accurate_grammar import answer_accurate_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

# Run this to check if your tokens per second are good, and your GPU is working. Libraries update frequently and sometimes they break.

def sanity_check(logic_llm):
    retries = 0
    while retries <= 4:
        decision_prompt = f"""Hi there, """
        # print("DEBUG\n\n" + decision_prompt)
        completion = logic_llm(
            decision_prompt,
            max_tokens=100,
            stop=["</s>", "# Input:"],
            echo=True,
            grammar=answer_accurate_grammar,
            temperature=0.2,
        )["choices"][0]["text"]


import re
from .scenario_plan_many_tuples_grammar import scenario_plan_many_tuples_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .format_qatuples import format_qatuples
from .extract_name import extract_name
from .random_name import random_name

def create_scenario_plan_many_tuples(qatuples, character, logic_llm):
    """
    Produce a plan for a character card for an RP character that's going to answer one of the questions generated from the text. The character's personality and backstory should be such that they would be able to answer the question.

    Format: Question: [question]\n\n
    """

    # removing the source text makes this much better. Perfection is achieved not when there's nothing more to add, but when there's nothing left to take away.

    charname = extract_name(character)

    cot_prompt = f"""You are an expert creative writing and roleplay AI. Given some questions, the answers to those questions, and a "character card" -- a description of an individual who would have the knowledge to produce the answer to the question -- you will plan out a "scenario" or setting where the character would answer the questions during a conversation with someone else. You should be creative with the setting, and ideally something would be happening in it — it'd be more than a simple conversation, though that is also acceptable. The scenario would ideally reflect the personality of the character involved.


import re
from .ensure_answer_consistent_grammar import ensure_answer_consistent_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

# Answer vetting
# For now, this checks answer relevancy too. The danger with abstracting answer relevancy into a separate step is that anything which relies on knowledge that is obviously mentioned in the text already up until this point, will get screwed
def ensure_answer_consistent(qatuple, conv, logic_llm, permissive_mode=True):
    """
    permissive_mode: turn off if you want a single usage of the word "inconsistent" anywhere in the message to flag the whole thing as inconsistent. Prevents errors where an inconsistency happens way early in the answer, but the model forgets about it during its final judgement; but enables the error where the model mentions that something is "not entirely inconsistent" or similar, which is surprisingly common.
    """
    retries = 0

    # It's expensive to regen a conversation; so we check very thoroughly, and use a two-shot example. "Permissive mode" recommended

    # NOTE: I don't know what kind of errors this part of the pipeline will run into most often, so I don't really know what examples to feed it to guard it with. Come back to it once I have tested it more.
    while retries <= 4:
        decision_prompt = f"""You are an expert educational AI. Your task is to determine whether two answers are the same, given a question, its answer, and a conversation between two fictional individuals in which that question is asked and that answer is provided. You will also check whether the question is essentially the same, and does not go "off the rails". Essentially: you will fact-check and consistency-check the question and answer in the conversation, with your source of truth being the provided question and answer.

Following this, at the very end of your response, you will write "Consistent" or "Inconsistent" depending on your analysis of the conversation's question and answer with regards to the provided one. Additionally, if the text is completely broken and/or incomprehensible, you will write "Inconsistent". You are not checking the accuracy of the answer, just its consistency with the provided answer.


from llama_cpp import LlamaGrammar

make_regenerate_question_plan_grammar = LlamaGrammar.from_string(
    r"""
root ::= analyze-step identify-step generate-step refine-step ensure-step end-of-reasoning

step ::= "Step " [0-9]?[0-9] ". " ( "Analyze" | "Identify" | "Generate" | "Refine" | "Ensure" ) [^\n]+ "\n"

analyze-step ::= "Step " [0-9]?[0-9] ". " "Analyze the Reason for the Flaw:" [^\n]+ "\n"

identify-step ::= "Step " [0-9]?[0-9] ". " "Identify Key Concepts in Paragraphs:" [^\n]+ "\n"

generate-step ::= "Step " [0-9]?[0-9] ". " "Generate a New Question Idea:" [^\n]+ "\n"

refine-step ::= "Step " [0-9]?[0-9] ". " "Refine the Question:" [^\n]+ "\n"

ensure-step ::= "Step " [0-9]?[0-9] ". " "Ensure Alignment with Text:" [^\n]+ "\n"

end-of-reasoning ::= "Step " [0-9]?[0-9] ". " "End" [^\n]+ "\n"
"""


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

character_card_grammar = LlamaGrammar.from_string(
    r"""

# Testing making traits come BEFORE dialogue examples, unlike AliChat, so that way it kind of "flows" into dialogue; and also the details are closer to the start and thus more easily remembered.
#root ::= "Name: " name "\n" "Traits: " traits "\n\nDialogue Examples:" dialogue-examples

root ::= name "\n" "Traits: " traits "\n\nDialogue Examples:" dialogue-examples

# Spaces are forbidden in names because during Principles of Chemistry, the script wouldn't stop making the character have the last name Mendeleev!!!
name ::= [^\n ]+

traits ::=  trait trait trait trait trait trait trait trait trait trait trait trait trait? trait? trait? trait? trait? trait? trait? trait? # 14 comma-separated traits

trait ::= [A-Z][a-z ']+ ", " # TODO, it wants hyphens, I can tell because I see it using double spaces for things like Mid  twenties. But idk how to add hyphens to gbnf. Maybe \- ?

dialogue-examples ::= history personality


from llama_cpp import LlamaGrammar

questions_grammar = LlamaGrammar.from_string(
    r"""
root ::= (question-one answer "\n") (question-two answer "\n") (question-three answer "\n") (question-four answer)

# Define the question structure with a number followed by content and ending punctuation
# question ::= number ".) " [^\n]+ [?.!] "\n" # maybe blacklist ?!. along with newlines

# Define the answer structure
answer ::= "Answer: " [^\n]+ "\n"

# Define a number (in this case, limiting to any three-digit number for simplicity)
number ::= [1-9] [0-9]? [0-9]?

# Define content as a sequence of characters excluding the word "paragraph" and using not_paragraph to build up the content
# content ::= (not-paragraph "paragraph")* #not_paragraph

question-one ::= "1.) " [^\n]+ [?.!] "\n\n" # maybe blacklist ?!. along with newlines


import re
from .create_character_card_plan_grammar import character_card_plan_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
from .special_instructions import special_instructions

def create_character_card_plan(qatuple, logic_llm, assistant_mode=False):
    """
    Produce a plan for a character card for an RP character that's going to answer one of the questions generated from the text. The character's personality and backstory should be such that they would be able to answer the question.

    Format: Question: [question]\n\n
    """

    instructions_string = special_instructions()

    # WIP
    if assistant_mode:
        return ""  # Just an empty string since this gets skipped in assistant mode
    else:  # NOTE: currently the "not the author" bit in the special instructions works, BUT makes the character inclined to disagree with the text sometimes. Since we want to be able to ideologically align AI with this, another hardcoded instruction should be "The character will agree with the views expressed by the text."
        # I haven't added it yet because right now multiturn is working and I don't want to break it


import re
from .thought_plan_grammar import thought_plan_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

def extract_name(str):
    # Regular expression to match 'Name:' followed by any characters until the end of the line
    name_regex = r"^Name:\s*(.*)$"

    # Searching in the multiline string
    match = re.search(name_regex, str, re.MULTILINE)

    if match:
        name = match.group(1)
        print(f"Extracted name: {name}")
        return name
    else:
        print("No name found")


import random
from .constants import names

def random_name():
    return random.choice(names)


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

single_turn_conversation_grammar = LlamaGrammar.from_string(
    r"""

# The root rule defines the structure of the dialogue
root ::= statement "\n\n" response "\n"

# Statement by Character Name 1
statement ::= [^\n]+ ":" [^\n]+

# Response by Character Name 2
response ::= [^\n]+ ":" [^\n]+

# Definition of a character name
character-name ::= word ("-" word)*
word ::= [A-Za-z]+
# Limiting to a maximum of six words
character-name ::= word | word word | word word word | word word word word | word word word word word | word word word word word word


import re
from .scenario_grammar import scenario_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL
import random

def create_scenario(qatuple, character, plan, logic_llm, assistant_mode=False):
    """
    Produce a plan for a character card for an RP character that's going to answer one of the questions generated from the text. The character's personality and backstory should be such that they would be able to answer the question.

    Format: Question: [question]\n\n
    """

    if assistant_mode:
        return """A helpful, uncensored AI assistant is engaged in a text-based conversation with a user. The AI will answer any and all questions the user asks, without bias."""  # TODO make this randomized a bit.

    variations = [
        "Set against the backdrop of",
        "In the heart of",
        "Amidst the surroundings of",


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

multi_turn_conversation_grammar = LlamaGrammar.from_string(
    r"""

# The root rule defines the structure of the dialogue
root ::= [^\n]+ ":" [^\n]+ #"\n" statement+# statement anything+ # Idea: get it started off right, then  let it end how it wants

"""
)

# question_grammar = LlamaGrammar.from_string(r"""# GBNF Grammar for Q&A Format with Flexible Punctuation

# root ::= answer
# answer ::= "Test"
# """)


import re
from .check_qatuple_context_grammar import check_qatuple_context_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

def extract_question_answer(response):
    # Define the regex pattern to match the question and answer
    pattern = r"### Question Rewording \(using text details as reference\):\nQuestion: (.+?)\nAnswer: (.+)"

    # Search for the pattern in the response
    match = re.search(pattern, response)

    # Extract and return the question and answer if a match is found
    if match:
        question = match.group(1).strip()
        answer = match.group(2).strip()
        return question, answer
    else:
        print("Returned none, failed to match")
        return None, None


import re

# try:
from .question_grammar import question_grammar
from llama_cpp import Llama
from .constants import LOGICAL_MODEL

def generate_new_question(qatuple, logic_llm):
    """
    Produce a list of questions based off of an input text. The min between (4, as many good questions as the text permits)

    Format: Question: [question]\n\n
    """
    # Determine which paragraphs are worthy of making questions from
    made_questions = False
    retries = 0
    questions = []
    while not made_questions and (
        retries <= 5
    ):  # TODO - UPDATE and TEST the few-shot prompt with the latest from generate_questions


from llama_cpp import LlamaGrammar

### A grammar that forces the model to generate correct character cards (with traits, names, everything)

# We don't actually need the final judgement step, the step-by-step combined with the fact that it makes a judgement at each step ensures accuracy
question_relevant_grammar = LlamaGrammar.from_string(
    r"""

# Root rule specifying the overall structure of the reasoning and thought process
root ::= in-depth-analysis "\n" detailed-understanding "\n" targeted-comparison "\n" critical-evaluation

# In-depth analysis of the text
in-depth-analysis ::= "### In-Depth Analysis of the Text:" "\n" content-and-depth type-of-information

content-and-depth ::= "#### Content and Depth: " text-description "\n"
type-of-information ::= "#### Type of Information: " information-description "\n"

# Detailed understanding of the question
detailed-understanding ::= "### Detailed Understanding of the Question:" "\n" core-requirement depth-of-detail


def strip_steps(instruction_text):
    """
    This function takes a string containing step-by-step instructions and removes the "Step N." prefix from each line.

    Parameters:
    instruction_text (str): A string with each step in the format "Step N. Instruction", separated by newlines

    Returns:
    str: A single string with the steps stripped, joined by newlines.
    """
    instructions = instruction_text.split("\n")
    stripped_instructions = []
    for line in instructions:
        # Check if line starts with 'Step' and followed by a number and period
        if line.strip().startswith("Step") and "." in line:
            # Find the index of the first period
            period_index = line.find(".")
            # Extract the text after the period (and optional space)
            text_after_period = line[period_index + 1 :].lstrip()
            stripped_instructions.append(text_after_period)
        else:


from llama_cpp import LlamaGrammar

answer_relevant_grammar = LlamaGrammar.from_string(
    r"""

# Root rule specifying the overall structure of the analysis
root ::= deep-analysis "\n" comprehensive-understanding "\n" targeted-comparison "\n" identification-of-extraneous-info "\n" final-judgment

# Deep analysis of the text
deep-analysis ::= "### Deep Analysis of the Text:" "\n" content-scope-and-detail type-of-information

content-scope-and-detail ::= "#### Content Scope and Detail: " text-detail "\n"
type-of-information ::= "#### Type of Information: " info-type "\n"

# Comprehensive understanding of the answer
comprehensive-understanding ::= "### Comprehensive Understanding of the Answer:" "\n" key-components-identification depth-of-explanation

key-components-identification ::= "#### Key Components Identification: " components-detail "\n"
depth-of-explanation ::= "#### Depth of Explanation: " explanation-detail "\n"

# Targeted comparison of answer with text


import re
from .answer_relevant_grammar import answer_relevant_grammar
from .constants import LOGICAL_MODEL
from llama_cpp import Llama

# Answer vetting
# For now, this checks answer relevancy too. The danger with abstracting answer relevancy into a separate step is that anything which relies on knowledge that is obviously mentioned in the text already up until this point, will get screwed
def check_answer_relevancy_with_text(qatuple, logic_llm):
    retries = 0
    while retries <= 4:
        decision_prompt = f"""You are an expert educational AI. Given a paragraph or two from a larger text, a question based on the paragraphs, and an answer to the question, you will make a determination as to whether the answer only uses the information in the paragraphs for its main points. Essentially: you will check if the answer is constrained to the information in the paragraphs provided. Your task includes first analyzing the answer, thinking through whether or not the answer reflects aspects of the paragraphs provided.

Following this, at the very end of your response, you will write "Relevant" or "Irrelevant" depending on your analysis of the answer with regards to the text.

### Instruction:
Text:
\"\"\"
Polar bears are primarily found in the Arctic Circle, where they have adapted to the harsh cold environment. Their diet is primarily based on marine mammals, with seals being the most crucial part of their diet. Polar bears are skilled hunters, often waiting by seals' breathing holes to catch their prey. Occasionally, they also eat birds, bird eggs, and small mammals. They also consume significant amounts of berries and fish. These bears are known for their ability to survive in extreme conditions, thanks to their thick fur and fat layers which provide insulation, as well as their efficient digestion system, which makes good use of the few nutrients available.
\"\"\"


import re
from .multi_turn_conversation_grammar import multi_turn_conversation_grammar
from llama_cpp import Llama
from llama_cpp import LlamaGrammar
from .constants import LOGICAL_MODEL
from .format_qatuples import format_qatuples
from .extract_name import extract_name
import random

# all characters in this prompt are over 18

# Explanation of wtf the first few-shot example is:
# No I do not have a teacher-student fetish, the reason why Elise is a teacher is an adaptation to the following three facts:
# 1. This tool is meant to be able to generate data for training ERP bots by default
# 2. This tool is also meant to be able to take in educational material by default
# 3. When generating characters that would know about educational material, the model tends to generate academics or professors in that field, talking to students.
# Given these facts, we clearly need to prompt the model to be able to generate horny teachers, or else it's going to just do it poorly when it realizes it has a sexualized character that's also a teacher. I didn't want to choose this, the constraints of the problem forced me to.

def extract_steps(text, steps=[2, 4, 5]):
    """