GameAI / context_parser.py
j-js's picture
Update context_parser.py
ffa674c verified
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"