| from __future__ import annotations |
|
|
| import re |
| from typing import Any, Dict, List, Optional |
|
|
|
|
| def _to_bool(value: str) -> bool: |
| return str(value or "").strip().lower() in {"true", "1", "yes", "y"} |
|
|
|
|
| def _looks_like_int(value: str) -> bool: |
| return bool(re.fullmatch(r"-?\d+", str(value or "").strip())) |
|
|
|
|
| def split_unity_message(text: str) -> Dict[str, Any]: |
| """ |
| Parses several Unity-style message formats and always returns a stable dict. |
| |
| Supported cases: |
| 1. Plain user text |
| 2. Hidden-context prefix + USER:/PROMPT:/MESSAGE: |
| 3. Structured multiline payload like: |
| hint |
| x/5 = 12 |
| 0 |
| solve |
| False |
| answer |
| answer |
| algebra |
| Quantitative |
| """ |
| raw = (text or "").strip() |
|
|
| result: Dict[str, Any] = { |
| "hidden_context": "", |
| "user_text": raw, |
| "question_text": "", |
| "hint_stage": 0, |
| "user_last_input_type": "", |
| "built_on_previous_turn": False, |
| "help_mode": "", |
| "intent": "", |
| "topic": "", |
| "category": "", |
| } |
|
|
| if not raw: |
| return result |
|
|
| |
| tagged_match = re.search(r"(?is)^(.*?)(?:\buser\b|\bprompt\b|\bmessage\b)\s*:\s*(.+)$", raw) |
| if tagged_match: |
| hidden = (tagged_match.group(1) or "").strip() |
| user = (tagged_match.group(2) or "").strip() |
| result["hidden_context"] = hidden |
| result["user_text"] = user |
| return result |
|
|
| |
| lines = [line.strip() for line in raw.splitlines() if line.strip()] |
| if len(lines) >= 9 and _looks_like_int(lines[2]) and lines[4].lower() in {"true", "false"}: |
| result["user_text"] = lines[0] |
| result["question_text"] = lines[1] |
| result["hint_stage"] = int(lines[2]) |
| result["user_last_input_type"] = lines[3] |
| result["built_on_previous_turn"] = _to_bool(lines[4]) |
| result["help_mode"] = lines[5] |
| result["intent"] = lines[6] |
| result["topic"] = lines[7] |
| result["category"] = lines[8] |
| return result |
|
|
| |
| def _extract_field(name: str) -> str: |
| match = re.search(rf"(?im)^\s*{re.escape(name)}\s*[:=]\s*(.+?)\s*$", raw) |
| return (match.group(1) or "").strip() if match else "" |
|
|
| question_text = _extract_field("question") or _extract_field("question_text") |
| user_text = _extract_field("user") or _extract_field("message") or _extract_field("prompt") |
| hint_stage_text = _extract_field("hint_stage") |
| user_last_input_type = _extract_field("user_last_input_type") |
| built_on_previous_turn = _extract_field("built_on_previous_turn") |
| help_mode = _extract_field("help_mode") |
| intent = _extract_field("intent") |
| topic = _extract_field("topic") |
| category = _extract_field("category") |
|
|
| if any([ |
| question_text, |
| user_text, |
| hint_stage_text, |
| user_last_input_type, |
| built_on_previous_turn, |
| help_mode, |
| intent, |
| topic, |
| category, |
| ]): |
| result["question_text"] = question_text |
| result["user_text"] = user_text or raw |
| result["hint_stage"] = int(hint_stage_text) if _looks_like_int(hint_stage_text) else 0 |
| result["user_last_input_type"] = user_last_input_type |
| result["built_on_previous_turn"] = _to_bool(built_on_previous_turn) |
| result["help_mode"] = help_mode |
| result["intent"] = intent |
| result["topic"] = topic |
| result["category"] = category |
| return result |
|
|
| |
| return result |
|
|
|
|
| def _extract_options(text: str) -> List[str]: |
| if not text: |
| return [] |
|
|
| lines = [line.strip() for line in text.splitlines() if line.strip()] |
| options: List[str] = [] |
|
|
| for line in lines: |
| if re.match(r"^[A-E][\)\.\:]\s*", line, flags=re.I): |
| options.append(re.sub(r"^[A-E][\)\.\:]\s*", "", line, flags=re.I).strip()) |
|
|
| if options: |
| return options |
|
|
| matches = re.findall(r"(?:^|\s)([A-E])[\)\.\:]\s*(.*?)(?=(?:\s+[A-E][\)\.\:])|$)", text, flags=re.I | re.S) |
| if matches: |
| return [m[1].strip() for m in matches if m[1].strip()] |
|
|
| return [] |
|
|
|
|
| def extract_game_context_fields(text: str) -> Dict[str, Any]: |
| raw = (text or "").strip() |
|
|
| result: Dict[str, Any] = { |
| "question": "", |
| "options": [], |
| "difficulty": None, |
| "category": None, |
| "money": None, |
| "has_choices": False, |
| "looks_like_quant": False, |
| } |
|
|
| if not raw: |
| return result |
|
|
| q_match = re.search(r"\bquestion\s*[:=]\s*(.+?)(?=\n[A-Za-z_ ]+\s*[:=]|\Z)", raw, flags=re.I | re.S) |
| if q_match: |
| result["question"] = q_match.group(1).strip() |
|
|
| opt_match = re.search(r"\b(?:options|choices|answers)\s*[:=]\s*(.+?)(?=\n[A-Za-z_ ]+\s*[:=]|\Z)", raw, flags=re.I | re.S) |
| if opt_match: |
| result["options"] = _extract_options(opt_match.group(1)) |
|
|
| if not result["options"]: |
| result["options"] = _extract_options(raw) |
|
|
| result["has_choices"] = len(result["options"]) > 0 |
|
|
| difficulty_match = re.search(r"\bdifficulty\s*[:=]\s*([A-Za-z0-9_\- ]+)", raw, flags=re.I) |
| if difficulty_match: |
| result["difficulty"] = difficulty_match.group(1).strip() |
|
|
| category_match = re.search(r"\b(?:category|topic)\s*[:=]\s*([A-Za-z0-9_\- /]+)", raw, flags=re.I) |
| if category_match: |
| result["category"] = category_match.group(1).strip() |
|
|
| money_match = re.search(r"\b(?:money|balance|bank)\s*[:=]\s*([\-]?\d+(?:\.\d+)?)", raw, flags=re.I) |
| if money_match: |
| try: |
| result["money"] = float(money_match.group(1)) |
| except Exception: |
| pass |
|
|
| lower = raw.lower() |
| result["looks_like_quant"] = any( |
| token in lower |
| for token in [ |
| "solve", |
| "equation", |
| "percent", |
| "%", |
| "ratio", |
| "probability", |
| "mean", |
| "median", |
| "algebra", |
| "integer", |
| "triangle", |
| "circle", |
| ] |
| ) |
|
|
| return result |
|
|
|
|
| def detect_intent(text: str, incoming_help_mode: Optional[str] = None) -> str: |
| forced = (incoming_help_mode or "").strip().lower() |
| if forced in { |
| "answer", |
| "hint", |
| "instruction", |
| "walkthrough", |
| "step_by_step", |
| "explain", |
| "method", |
| "definition", |
| "concept", |
| }: |
| return forced |
|
|
| t = (text or "").strip().lower() |
|
|
| if not t: |
| return "answer" |
|
|
| if ( |
| re.search(r"\bdefine\b", t) |
| or re.search(r"\bdefinition\b", t) |
| or re.search(r"\bwhat does\b", t) |
| or re.search(r"\bwhat is meant by\b", t) |
| ): |
| return "definition" |
|
|
| if re.search(r"\bhint\b", t) or re.search(r"\bclue\b", t) or re.search(r"\bnudge\b", t): |
| return "hint" |
|
|
| if ( |
| re.search(r"\bfirst step\b", t) |
| or re.search(r"\bnext step\b", t) |
| or re.search(r"\bwhat should i do first\b", t) |
| or re.search(r"\bgive me the first step\b", t) |
| or re.search(r"\bspecific step\b", t) |
| ): |
| return "instruction" |
|
|
| if ( |
| re.search(r"\bwalk ?through\b", t) |
| or re.search(r"\bstep by step\b", t) |
| or re.search(r"\bfull working\b", t) |
| or re.search(r"\bwork through\b", t) |
| ): |
| return "walkthrough" |
|
|
| if re.search(r"\bexplain\b", t) or re.search(r"\bwhy\b", t): |
| return "explain" |
|
|
| if ( |
| re.search(r"\bmethod\b", t) |
| or re.search(r"\bapproach\b", t) |
| or re.search(r"\bhow do i solve\b", t) |
| or re.search(r"\bhow to solve\b", t) |
| or re.search(r"\bhow do i do this\b", t) |
| ): |
| return "method" |
|
|
| if ( |
| re.search(r"\bconcept\b", t) |
| or re.search(r"\bprinciple\b", t) |
| or re.search(r"\brule\b", t) |
| or re.search(r"\bwhat is the idea\b", t) |
| ): |
| return "concept" |
|
|
| if ( |
| re.search(r"\bsolve\b", t) |
| or re.search(r"\bwhat is\b", t) |
| or re.search(r"\bfind\b", t) |
| or re.search(r"\bgive (?:me )?the answer\b", t) |
| or re.search(r"\bjust the answer\b", t) |
| or re.search(r"\banswer only\b", t) |
| or re.search(r"\bcalculate\b", t) |
| ): |
| return "answer" |
|
|
| return "answer" |
|
|
|
|
| def intent_to_help_mode(intent: str) -> str: |
| if intent in {"walkthrough", "step_by_step"}: |
| return "walkthrough" |
| if intent in {"explain", "method", "concept"}: |
| return "explain" |
| if intent == "hint": |
| return "hint" |
| if intent in {"definition", "instruction"}: |
| return intent |
| return "answer" |