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 # Case 1: hidden/system context followed by USER:/PROMPT:/MESSAGE: 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 # Case 2: exact structured Unity payload block 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 # Case 3: field-based payload 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 # Fallback: plain message 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"