Files
2024-10-24 10:47:47 -07:00

488 lines
14 KiB
Python

import re
import json
import langdetect
from typing import List
"""
This module contains functions to verify constraints in the responses generated by the model.
It covers all 25 constraints from the IFEval taxonomy. To be used either for eval or for ground truth rewards.
"""
# include keywords: Include keywords {keyword1}, {keyword2} in your response
def verify_keywords(text, keyword_list):
"""
Verify if the response contains all the specified keywords.
Args:
response (str): The response text to check
keyword_list (list): A list of keywords to check for
Returns:
bool: True if all keywords are present in the response, False otherwise
"""
# Convert response to lowercase for case-insensitive matching
response_lower = text.lower()
# Check if all keywords are present in the response
return all(keyword.lower() in response_lower for keyword in keyword_list)
# Keyword Frequency: In your response, the word {word} should appear {N} times.
def verify_keyword_frequency(text, word, N):
"""
Verifies if a keyword appears exactly N times in the given text.
Args:
text (str): The text to analyze
keyword (str): The keyword to count
expected_count (int): The expected number of occurrences
Returns:
tuple: (bool, int) - (Whether constraint is met, actual count found)
"""
# Convert text to lowercase to make the search case-insensitive
text = text.lower()
keyword = word.lower()
# Split text into words and remove punctuation
import re
words = re.findall(r'\b\w+\b', text)
# Count actual occurrences
actual_count = sum(1 for word in words if word == keyword)
# Check if constraint is met
constraint_met = actual_count == N
return constraint_met
# Forbidden Words: Do not include keywords {forbidden words} in the response.
def validate_forbidden_words(text, forbidden_words):
"""
Validates that the text does not contain any of the specified forbidden words.
Args:
text (str): The text to check for forbidden words
forbidden_words (list[str]): A list of forbidden words
Returns:
tuple[bool, list[str]]: A tuple containing:
- Boolean indicating if any forbidden words are present
- List of forbidden words found in the text
Example:
text = "This is a message that should not contain any bad words"
forbidden_words = ["bad", "evil", "harmful"]
result = validate_forbidden_words(text, forbidden_words)
"""
# Convert text to lowercase for case-insensitive matching
text_lower = text.lower()
# Check each forbidden word
found_words = [word for word in forbidden_words if word.lower() in text_lower]
# Return results
return len(found_words) == 0
# Letter Frequency : In your response, the letter {letter} should appear {N} times.
def verify_letter_frequency(text: str, letter: str, N: int) -> bool:
"""
Verifies if a given letter appears exactly the specified number of times in the text.
Args:
text (str): The text to check
letter (str): The letter to count (case-sensitive)
target_count (int): The expected number of occurrences
Returns:
bool: True if the constraint is met, False otherwise
Example:
>>> verify_letter_frequency("hello world", "l", 3)
True
>>> verify_letter_frequency("hello world", "o", 2)
True
>>> verify_letter_frequency("hello world", "x", 0)
True
"""
if len(letter) != 1:
raise ValueError("Letter parameter must be a single character")
actual_count = text.count(letter)
return actual_count == N
# Response Language: Your ENTIRE response should be in {language}, no other language is allowed.
def validate_response_language(text, language):
"""
Validates that the entire response is in the specified language.
Args:
text (str): The text to check
language (str): The language code (e.g., 'en' for English)
Returns:
bool: True if the response is entirely in the specified language, False otherwise
Example:
text = "This is an English sentence"
language = "en"
result = validate_response_language(text, language)
"""
from langdetect import detect
# Detect the language of the text
detected_language = detect(text)
# Check if the detected language matches the expected language
return detected_language == language
# Number Paragraphs: Your response should contain {N} paragraphs. You separate paragraphs using the markdown divider:
# * * *
def verify_paragraph_count(text: str, N: int) -> bool:
"""
Verifies that a text contains the expected number of paragraphs,
where paragraphs are separated by markdown dividers '* * *'
Args:
text (str): The text to analyze
expected_count (int): Expected number of paragraphs
Returns:
bool: True if the text contains exactly the expected number of paragraphs,
False otherwise
Example:
text = "First paragraph\n* * *\nSecond paragraph"
verify_paragraph_count(text, 2)
True
"""
def clean_text(text: str) -> str:
"""Remove extra whitespace and normalize line endings"""
return '\n'.join(line.strip() for line in text.splitlines()).strip()
# Clean the input text
text = clean_text(text)
# Split text by markdown divider
# Add 1 to count since n dividers create n+1 paragraphs
paragraphs = text.split('* * *')
actual_count = len(paragraphs)
# Verify each split resulted in non-empty content
valid_paragraphs = [p.strip() for p in paragraphs if p.strip()]
if len(valid_paragraphs) != actual_count:
return False
return actual_count == N
# Number Words: Answer with at least / around / at most {N} words
def validate_word_constraint(text: str, N: int, quantifier: str) -> bool:
"""
Validates if a text meets specified word count constraints.
Args:
text (str): The text to check
count (int): The target word count
qualifier (str): The type of constraint ('at least', 'around', 'at most')
Returns:
bool: True if the constraint is met, False otherwise
Raises:
ValueError: If an invalid qualifier is provided
"""
# Remove extra whitespace and split into words
words = text.strip().split()
actual_count = len(words)
# Define tolerance for "around" qualifier (±10% of target count)
tolerance = max(round(N * 0.1), 1)
if quantifier == "at least":
return actual_count >= N
elif quantifier == "at most":
return actual_count <= N
elif quantifier == "around":
return abs(actual_count - N) <= tolerance
else:
return False
# Number Sentences: Answer with at least / around / at most {N} sentences.
def verify_sentence_constraint(text: str, N: int, quantifier: str) -> bool:
"""
Verifies if a text contains the expected number of sentences.
Args:
text (str): The text to analyze
N (int): The expected number of sentences
quantifier (str): The quantifier ('at least', 'around', 'at most')
Returns:
bool: True if the text contains the expected number of sentences, False otherwise
"""
# Split the text into sentences
sentences = re.split(r'(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\?)\s', text)
# Count the number of sentences
actual_count = len(sentences)
# Check if the actual count matches the expected count based on the quantifier
if quantifier == 'at least':
return actual_count >= N
elif quantifier == 'around':
return abs(actual_count - N) <= 1
elif quantifier == 'at most':
return actual_count <= N
else:
return False
# Number Paragraphs + First Word in i-th Paragraph: There should be {N} paragraphs. Paragraphs and only paragraphs
# are separated with each other by two line breaks. The {i}-th paragraph must start with word {first word}.
def validate_paragraphs(text, N, first_word, i):
"""
Validates that a text contains the expected number of paragraphs and that the i-th paragraph starts with a specific
word.
Args:
text (str): The text to analyze
N (int): The expected number of paragraphs
first_word (str): The expected first word of the i-th paragraph
i (int): The index of the paragraph to check (1-indexed)
Returns:
bool: True if the text meets the paragraph and first word requirements, False otherwise
"""
# Split the text into paragraphs
paragraphs = text.split('\n\n')
# Check if the number of paragraphs is as expected
if len(paragraphs) != N:
return False
# Check if the i-th paragraph starts with the specified first word
if paragraphs[i - 1].strip().startswith(first_word):
return True
return False
# Postscript: At the end of your response, please explicitly add a postscript starting with {postscript marker}
def verify_postscript(text, postscript_marker):
"""
Verifies if a text contains a postscript starting with '{postscript marker}'
Args:
text (str): The text to verify
Returns:
bool: True if the text contains a valid postscript, False otherwise
"""
# Check if the text contains the postscript marker
if postscript_marker in text:
# Get the index of the marker
marker_index = text.find(postscript_marker)
# Check if the marker appears near the end
remaining_text = text[marker_index:].strip()
# Verify it's not just the marker alone
return len(remaining_text) > len(postscript_marker)
return False
# Number Placeholder: The response must contain at least {N} placeholders represented by square brackets,
# such as [address].
def validate_placeholders(text: str, N: int) -> tuple[bool, List[str]]:
"""
Validates if a text contains at least the specified number of placeholders in square brackets.
Args:
text (str): The text to check for placeholders
min_placeholders (int): Minimum number of placeholders required
Returns:
tuple[bool, List[str]]: A tuple containing:
- Boolean indicating if the text meets the placeholder requirement
- List of found placeholders
Example:
>>> text = "Hello [name], your [item] will be delivered to [address]"
>>> validate_placeholders(text, 2)
(True, ['name', 'item', 'address'])
"""
# Find all placeholders using regex
pattern = r'\[(.*?)\]'
placeholders = re.findall(pattern, text)
# Check if the number of placeholders meets the requirement
has_enough = len(placeholders) >= N
return has_enough, placeholders
# Number Bullets: Your answer must contain exactly {N} bullet points. Use the markdown bullet points such as: * This
# is a point.
def verify_bullet_points(text: str, N: int) -> tuple[bool, str]:
"""
Verifies if a text contains exactly N bullet points in markdown format.
Returns a tuple of (is_valid, message).
Args:
text (str): The text to check
expected_count (int): The expected number of bullet points
Returns:
tuple[bool, str]: (True if constraint is met, explanation message)
"""
# Split text into lines and count lines starting with * or -
lines = text.split('\n')
bullet_points = [line.strip() for line in lines if line.strip().startswith(('*', '-'))]
actual_count = len(bullet_points)
if actual_count == N:
return True
else:
return False
# Title: Your answer must contain a title, wrapped in double angular brackets, such as <<poem of joy>>.
def validate_title(text: str) -> bool:
pattern = r"<<(.*?)>>"
matches = re.findall(pattern, text)
if len(matches) > 0:
return True
else:
return False
# Choose: From Answer with one of the following options: {options}
def validate_choice(text: str, options: list) -> bool:
for option in options:
if text in option:
return True
return False
# Minimum Number Highlighted Section: Highlight at least {N} sections in your answer with markdown, i.e. *highlighted
# section*
def validate_highlighted_sections(text: str, N: int) -> bool:
pattern = r"\*(.*?)\*"
matches = re.findall(pattern, text)
if len(matches) >= N:
return True
else:
return False
# Multiple Sections: Your response must have {N} sections. Mark the beginning of each section with {section splitter} X.
def validate_sections(text: str, N: int, section_splitter: str) -> bool:
sections = text.split(section_splitter)
# The first section might not start with the splitter, so we adjust for this
if sections[0] == '':
sections.pop(0)
if len(sections) == N:
return True
else:
return False
# JSON Format : Entire output should be wrapped in JSON format.
def validate_json_format(text: str) -> bool:
try:
json_object = json.loads(text)
except ValueError as e:
return False
return True
# Repeat Prompt: First, repeat the request without change, then give your answer (do not say anything before
# repeating the request; the request you need to repeat does not include this sentence)
def validate_repeat_prompt(text: str, original_prompt: str) -> bool:
if text.startswith(original_prompt):
return True
else:
return False
# Two Responses: Give two different responses. Responses and only responses should be separated by 6 asterisk
# symbols: ******.
def validate_two_responses(text: str) -> bool:
if text.count('******') == 1:
response_list = text.split('******')
first_response = response_list[0].strip()
second_response = response_list[1].strip()
if first_response != second_response:
return True
return False
# All Uppercase: Your entire response should be in English, capital letters only.
def validate_uppercase(text: str) -> bool:
# Check if the response is the same as the uppercase version of the response
if text == text.upper():
return True
else:
return False
# All Lowercase: Your entire response should be in English, and in all lowercase letters. No capital letters are
# allowed.
def validate_lowercase(text: str) -> bool:
# Check if the response is the same as the lowercase version of the response
if text == text.lower():
return True
else:
return False
# Frequency of All-capital Words: In your response, words with all capital letters should appear at least / around /
# at most {N} times.
def validate_frequency_capital_words(text: str, N: int, quantifier: str) -> bool:
words = re.findall(r'\b[A-Z]+\b', text)
if quantifier == 'at least':
return len(words) >= N
elif quantifier == 'around':
return len(words) == N
elif quantifier == 'at most':
return len(words) <= N
else:
return False
# End Checker: Finish your response with this exact phrase {end phrase}. No other words should follow this phrase.
def validate_end(text: str, end_phrase: str) -> bool:
# Check if the response ends with the end phrase
if text.endswith(end_phrase):
return True
else:
return False
# Quotation: Wrap your entire response with double quotation marks.
def validate_quotation(text: str) -> bool:
if text.startswith('"') and text.endswith('"'):
return True
else:
return False
# No Commas: In your entire response, refrain from the use of any commas.
def validate_no_commas(text: str) -> bool:
if ',' not in text:
return True
else:
return False