Spaces:
Paused
Paused
Download guardrails.py from TechAvenger/MathMate: direct link, hf CLI and curl.
- Browser
- Download file 7.94 kB
-
https://huggingface.co/spaces/TechAvenger/MathMate/resolve/main/guardrails.py
- Command line
-
hf download hf://spaces/TechAvenger/MathMate/guardrails.py
-
curl -L -o guardrails.py https://huggingface.co/spaces/TechAvenger/MathMate/resolve/main/guardrails.py
7.94 kB
| """ | |
| Guardrails: input gate, leak check, refusal and fallback templates. | |
| """ | |
| import re | |
| from typing import Optional, Tuple, List, Dict, Any | |
| # --- Input Gate (code checks) --- | |
| INJECTION_PATTERNS = [ | |
| r"ignore\s+previous\s+instructions", | |
| r"ignore\s+all\s+instructions", | |
| r"system\s+prompt", | |
| r"reveal\s+the\s+solution", | |
| r"reveal\s+hidden", | |
| r"just\s+give\s+me\s+the\s+answer", | |
| r"act\s+as\s+", | |
| ] | |
| NUMBER_WORDS = { | |
| "zero": 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5, | |
| "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10, | |
| "eleven": 11, "twelve": 12, "thirteen": 13, "fourteen": 14, "fifteen": 15, | |
| "sixteen": 16, "seventeen": 17, "eighteen": 18, "nineteen": 19, "twenty": 20, | |
| "thirty": 30, "forty": 40, "fifty": 50, "sixty": 60, "seventy": 70, | |
| "eighty": 80, "ninety": 90, "hundred": 100, "thousand": 1000, | |
| "million": 1000000, "billion": 1000000000, | |
| } | |
| # Refusal messages (to be used verbatim) | |
| REFUSALS = { | |
| "empty": "I can't help with an empty problem. Please provide a math word problem.", | |
| "off_topic": "This doesn't look like a math word problem. Please provide a problem with numbers and a question.", | |
| "prompt_injection": "I can't process that request. Please provide a genuine math word problem.", | |
| } | |
| # Safe fallback hints | |
| SAFE_FALLBACK = { | |
| 1: "What information is given in the problem? What are you being asked to find?", | |
| 2: "What mathematical concept or formula might apply here? What's the first step?", | |
| 3: "Let me walk through the solution step by step.", | |
| } | |
| def input_gate_code(text: str) -> Optional[str]: | |
| """ | |
| Code-based input gate checks. | |
| Returns rejection category ("empty", "off_topic", "prompt_injection") or None if allowed. | |
| """ | |
| if not text or not text.strip(): | |
| return "empty" | |
| text_stripped = text.strip() | |
| # Length checks | |
| if len(text_stripped) < 15: | |
| return "off_topic" | |
| if len(text_stripped) > 1500: | |
| return "off_topic" | |
| # Prompt injection check | |
| text_lower = text_stripped.lower() | |
| for pattern in INJECTION_PATTERNS: | |
| if re.search(pattern, text_lower, re.IGNORECASE): | |
| return "prompt_injection" | |
| # Check for at least one digit or number word | |
| has_digit = bool(re.search(r"\d", text_stripped)) | |
| has_number_word = any(word in text_lower for word in NUMBER_WORDS.keys()) | |
| if not has_digit and not has_number_word: | |
| return "off_topic" | |
| return None | |
| # --- Leak Check (code checks) --- | |
| PHRASE_BLOCKLIST = [ | |
| "the answer is", | |
| "answer:", | |
| "so the result is", | |
| "final answer", | |
| ] | |
| def _normalize_number_text(text: str) -> str: | |
| """Normalize numeric text: strip units, commas, currency symbols.""" | |
| # Remove currency symbols | |
| text = re.sub(r"[\$\£\€\¥]", "", text) | |
| # Remove commas | |
| text = text.replace(",", "") | |
| # Remove units (anything after a number that's letters) | |
| text = re.sub(r"(\d+(?:\.\d+)?)\s*[a-zA-Z]+", r"\1", text) | |
| return text.strip() | |
| def _extract_numbers_from_text(text: str) -> List[str]: | |
| """Extract all numbers (integers, decimals, fractions) as normalized strings.""" | |
| numbers = [] | |
| # Pattern for fractions (e.g., 3/4, 15/8) | |
| for match in re.finditer(r"\b(\d+)\s*/\s*(\d+)\b", text): | |
| num = int(match.group(1)) | |
| den = int(match.group(2)) | |
| if den != 0: | |
| numbers.append(f"{num}/{den}") | |
| # Also add decimal equivalent | |
| numbers.append(f"{num/den:.10f}".rstrip('0').rstrip('.')) | |
| # Pattern for decimals and integers | |
| for match in re.finditer(r"\b(\d+(?:\.\d+)?)\b", text): | |
| val = match.group(1) | |
| # Normalize: strip leading zeros but keep at least one digit | |
| val_norm = val.lstrip('0') | |
| if val_norm == '' or val_norm.startswith('.'): | |
| val_norm = '0' + val_norm | |
| numbers.append(val_norm) | |
| return numbers | |
| def _number_to_words(n: float) -> List[str]: | |
| """Convert a number to possible word forms.""" | |
| words = [] | |
| # Integer form | |
| if n == int(n): | |
| words.append(str(int(n))) | |
| # Decimal form | |
| words.append(f"{n:.10f}".rstrip('0').rstrip('.')) | |
| # Word form for small integers | |
| if n == int(n) and 0 <= int(n) <= 20: | |
| for word, val in NUMBER_WORDS.items(): | |
| if val == int(n): | |
| words.append(word) | |
| return words | |
| def _get_answer_variants(answer: str) -> List[str]: | |
| """Build all variants of the answer for matching.""" | |
| variants = set() | |
| normalized = _normalize_number_text(answer) | |
| variants.add(normalized) | |
| # Try to parse as number | |
| try: | |
| # Handle fractions | |
| if '/' in normalized: | |
| parts = normalized.split('/') | |
| num = float(parts[0]) | |
| den = float(parts[1]) | |
| if den != 0: | |
| val = num / den | |
| variants.update(_number_to_words(val)) | |
| # Add fraction variants | |
| variants.add(f"{int(num)}/{int(den)}") | |
| else: | |
| val = float(normalized) | |
| variants.update(_number_to_words(val)) | |
| except (ValueError, ZeroDivisionError): | |
| pass | |
| return list(variants) | |
| def _get_last_step_result(solution: str) -> Optional[str]: | |
| """Extract the last step's result from a solution.""" | |
| lines = solution.strip().split('\n') | |
| for line in reversed(lines): | |
| line = line.strip() | |
| if '=' in line: | |
| # Get RHS of last equation | |
| rhs = line.split('=')[-1].strip() | |
| return _normalize_number_text(rhs) | |
| return None | |
| def leak_check_code(hint_text: str, solution: str, problem_text: str) -> Tuple[bool, str]: | |
| """ | |
| Check if hint leaks the answer. | |
| Returns (is_leak, reason). | |
| """ | |
| hint_lower = hint_text.lower() | |
| # Phrase blocklist check (always applies) | |
| for phrase in PHRASE_BLOCKLIST: | |
| if phrase.lower() in hint_lower: | |
| return True, f"Forbidden phrase detected: '{phrase}'" | |
| # Get answer variants | |
| answer_variants = _get_answer_variants(solution) | |
| # Get problem numbers (to allow numbers that appear in the problem) | |
| problem_numbers = set(_extract_numbers_from_text(problem_text)) | |
| problem_normalized = _normalize_number_text(problem_text) | |
| # Check if answer number appears as standalone token in hint | |
| hint_numbers = set(_extract_numbers_from_text(hint_text)) | |
| for variant in answer_variants: | |
| if variant in hint_numbers: | |
| # Check if this number appears in the problem text | |
| if variant not in problem_numbers: | |
| return True, f"Answer number '{variant}' appears in hint but not in problem" | |
| # Level 2 check: last step's result | |
| last_step_result = _get_last_step_result(solution) | |
| if last_step_result: | |
| last_step_variants = _get_answer_variants(last_step_result) | |
| for variant in last_step_variants: | |
| if variant in hint_numbers: | |
| if variant not in problem_numbers: | |
| return True, f"Last step result '{variant}' appears in hint but not in problem" | |
| return False, "No leak detected" | |
| def normalise_numeric_equal(a: str, b: str) -> bool: | |
| """ | |
| Compare two solver outputs numerically. | |
| Normalizes both and checks if they represent the same value within tolerance. | |
| """ | |
| try: | |
| a_norm = _normalize_number_text(a) | |
| b_norm = _normalize_number_text(b) | |
| # Try parsing as fractions first | |
| def parse_val(s: str) -> Optional[float]: | |
| if '/' in s: | |
| parts = s.split('/') | |
| return float(parts[0]) / float(parts[1]) | |
| return float(s) | |
| a_val = parse_val(a_norm) | |
| b_val = parse_val(b_norm) | |
| if a_val is not None and b_val is not None: | |
| return abs(a_val - b_val) < 1e-6 | |
| # Fallback to string comparison | |
| return a_norm == b_norm | |
| except (ValueError, ZeroDivisionError): | |
| return a == b |