GameAI / formatting.py
j-js's picture
Update formatting.py
f11d9f1 verified
raw
history blame
15.7 kB
from __future__ import annotations
import re
from typing import Any, List
def style_prefix(tone: float) -> str:
if tone < 0.2:
return ""
if tone < 0.45:
return "Let’s solve it efficiently."
if tone < 0.75:
return "Let’s work through it."
return "You’ve got this — let’s solve it cleanly."
def _clean_lines(core: str) -> list[str]:
lines = []
for line in (core or "").splitlines():
cleaned = line.strip()
if cleaned:
lines.append(cleaned)
return lines
def _normalize_key(text: str) -> str:
text = (text or "").strip().lower()
text = text.replace("’", "'")
text = re.sub(r"\s+", " ", text)
return text
def _is_wrapper_line(line: str) -> bool:
key = _normalize_key(line).rstrip(":")
wrapper_lines = {
"let's work through it.",
"lets work through it.",
"let's solve it efficiently.",
"lets solve it efficiently.",
"you've got this — let's solve it cleanly.",
"youve got this — lets solve it cleanly.",
"you’ve got this — let’s solve it cleanly.",
"walkthrough",
"hint",
"steps",
"method",
"explanation",
"key idea",
"question breakdown",
"here’s what the question is really asking",
"here's what the question is really asking",
"let’s break down what the question is asking",
"lets break down what the question is asking",
"you’ve got this — here’s what the question is really asking",
"you've got this — here's what the question is really asking",
"what to identify first",
"set-up path",
"setup path",
"first move",
"next hint",
"watch out for",
"variables to define",
"equations to form",
"key teaching points",
"how to build it",
}
return key in wrapper_lines
def _strip_leading_wrapper_lines(lines: list[str]) -> list[str]:
cleaned = list(lines)
while cleaned and _is_wrapper_line(cleaned[0]):
cleaned.pop(0)
return cleaned
def _dedupe_lines(lines: list[str]) -> list[str]:
seen = set()
output = []
for line in lines:
key = _normalize_key(line)
if key and key not in seen:
seen.add(key)
output.append(line.strip())
return output
def _limit_lines_for_verbosity(lines: list[str], verbosity: float, help_mode: str) -> list[str]:
if not lines:
return lines
if help_mode == "hint":
return lines[:1]
if verbosity < 0.2:
return lines[:1]
if verbosity < 0.45:
return lines[:2]
if verbosity < 0.75:
return lines[:3]
return lines
def _extract_topic_from_lines(lines: list[str]) -> str:
joined = " ".join(lines).lower()
if any(word in joined for word in ["equation", "variable", "isolate", "algebra"]):
return "algebra"
if any(word in joined for word in ["percent", "percentage"]):
return "percent"
if any(word in joined for word in ["ratio", "proportion"]):
return "ratio"
if any(word in joined for word in ["probability", "outcome"]):
return "probability"
if any(word in joined for word in ["mean", "median", "average"]):
return "statistics"
if any(word in joined for word in ["triangle", "circle", "angle", "area", "perimeter"]):
return "geometry"
if any(word in joined for word in ["integer", "factor", "multiple", "prime", "remainder"]):
return "number_theory"
return "general"
def _why_line(topic: str, lines: list[str]) -> str:
joined = " ".join(lines).lower()
if topic == "algebra":
if "equation" in joined or "isolate" in joined or "variable" in joined:
return "Why: this works because inverse operations undo what is attached to the variable while keeping the equation balanced."
return "Why: the goal is to isolate the variable without changing the balance of the relationship."
if topic == "percent":
return "Why: percent relationships depend on identifying the correct base value before calculating the change or part."
if topic == "ratio":
return "Why: ratios compare parts consistently, so the relationship must stay proportional."
if topic == "probability":
return "Why: probability compares favorable outcomes to the total number of possible outcomes."
if topic == "statistics":
return "Why: statistical measures describe the distribution, so the method depends on what feature the question asks for."
if topic == "geometry":
return "Why: geometry problems depend on the properties of the figure and the relationships between its parts."
if topic == "number_theory":
return "Why: number properties such as divisibility, factors, and remainders follow fixed rules that guide the method."
return "Why: focus on the structure of the problem before doing any calculations."
def _tone_adjust_line(line: str, tone: float) -> str:
line = (line or "").strip()
if not line:
return line
if tone < 0.25:
line = re.sub(r"^let’s\s+", "", line, flags=re.IGNORECASE)
line = re.sub(r"^let's\s+", "", line, flags=re.IGNORECASE)
line = re.sub(r"^here is the idea in context:\s*", "", line, flags=re.IGNORECASE)
return line.strip()
return line
def format_reply(core: str, tone: float, verbosity: float, transparency: float, help_mode: str) -> str:
prefix = style_prefix(tone)
core = (core or "").strip()
if not core:
return prefix or "Start with the structure of the problem."
lines = _clean_lines(core)
lines = _strip_leading_wrapper_lines(lines)
lines = [_tone_adjust_line(line, tone) for line in lines]
lines = [line for line in lines if line]
lines = _dedupe_lines(lines)
lines = _limit_lines_for_verbosity(lines, verbosity, help_mode)
if not lines:
return prefix or "Start with the structure of the problem."
topic = _extract_topic_from_lines(lines)
output: list[str] = []
if prefix:
output.append(prefix)
output.append("")
if help_mode == "hint":
output.append("Hint:")
output.extend(lines[:1])
if transparency >= 0.8:
output.append("")
output.append(_why_line(topic, lines[:1]))
return "\n".join(output).strip()
if help_mode == "walkthrough":
output.append("Walkthrough:")
output.extend(lines)
if transparency >= 0.8:
output.append("")
output.append(_why_line(topic, lines))
return "\n".join(output).strip()
if help_mode in {"step_by_step", "method", "explain", "concept"}:
label = {
"step_by_step": "Steps:",
"method": "Method:",
"explain": "Explanation:",
"concept": "Key idea:",
}.get(help_mode, "Explanation:")
output.append(label)
output.extend(lines)
if transparency >= 0.75:
output.append("")
output.append(_why_line(topic, lines))
return "\n".join(output).strip()
output.extend(lines)
if transparency >= 0.85 and help_mode != "answer":
output.append("")
output.append(_why_line(topic, lines))
return "\n".join(output).strip()
def _explainer_header(tone: float) -> str:
if tone < 0.2:
return "Question breakdown:"
if tone < 0.45:
return "Here’s what the question is really asking:"
if tone < 0.75:
return "Let’s break down what the question is asking:"
return "You’ve got this — here’s what the question is really asking:"
def _clean_section_items(items: List[str]) -> List[str]:
cleaned = []
seen = set()
for item in items or []:
text = (item or "").strip()
if not text:
continue
if _is_wrapper_line(text):
continue
key = _normalize_key(text)
if key in seen:
continue
seen.add(key)
cleaned.append(text)
return cleaned
def _append_section(lines: List[str], title: str, items: List[str], limit: int) -> None:
cleaned = _clean_section_items(items)
if not cleaned:
return
lines.append("")
lines.append(title)
for item in cleaned[:limit]:
lines.append(f"- {item}")
def _coerce_string(value: Any) -> str:
return (value or "").strip() if isinstance(value, str) else ""
def _coerce_list(value: Any) -> List[str]:
if not value:
return []
if isinstance(value, list):
return [str(v).strip() for v in value if str(v).strip()]
if isinstance(value, tuple):
return [str(v).strip() for v in value if str(v).strip()]
if isinstance(value, str):
text = value.strip()
return [text] if text else []
return []
def _get_scaffold(result: Any):
return getattr(result, "scaffold", None)
def _explainer_topic(result: Any) -> str:
topic = _coerce_string(getattr(result, "topic", ""))
if topic:
return topic
joined = " ".join(
_coerce_list(getattr(result, "teaching_points", []))
+ _coerce_list(getattr(result, "givens", []))
+ _coerce_list(getattr(result, "relationships", []))
)
return _extract_topic_from_lines([joined]) if joined else "general"
def _build_scaffold_sections(
result: Any,
verbosity: float,
transparency: float,
) -> List[str]:
output: List[str] = []
scaffold = _get_scaffold(result)
if scaffold is None:
return output
ask = _coerce_string(getattr(scaffold, "ask", ""))
setup_actions = _coerce_list(getattr(scaffold, "setup_actions", []))
intermediate_steps = _coerce_list(getattr(scaffold, "intermediate_steps", []))
first_move = _coerce_string(getattr(scaffold, "first_move", ""))
next_hint = _coerce_string(getattr(scaffold, "next_hint", ""))
variables_to_define = _coerce_list(getattr(scaffold, "variables_to_define", []))
equations_to_form = _coerce_list(getattr(scaffold, "equations_to_form", []))
common_traps = _coerce_list(getattr(scaffold, "common_traps", []))
if ask:
output.append("")
output.append("What to identify first:")
output.append(f"- {ask}")
if setup_actions:
output.append("")
output.append("Set-up path:")
limit = 2 if verbosity < 0.4 else 3 if verbosity < 0.75 else 5
for item in setup_actions[:limit]:
output.append(f"- {item}")
if verbosity >= 0.55 and intermediate_steps:
output.append("")
output.append("How to build it:")
limit = 2 if verbosity < 0.75 else 4
for item in intermediate_steps[:limit]:
output.append(f"- {item}")
if first_move:
output.append("")
output.append("First move:")
output.append(f"- {first_move}")
if next_hint and (transparency >= 0.35 or verbosity >= 0.45):
output.append("")
output.append("Next hint:")
output.append(f"- {next_hint}")
if verbosity >= 0.65 and variables_to_define:
output.append("")
output.append("Variables to define:")
for item in variables_to_define[:3]:
output.append(f"- {item}")
if transparency >= 0.55 and equations_to_form:
output.append("")
output.append("Equations to form:")
for item in equations_to_form[:3]:
output.append(f"- {item}")
if transparency >= 0.6 or verbosity >= 0.75:
if common_traps:
output.append("")
output.append("Watch out for:")
for item in common_traps[:4]:
output.append(f"- {item}")
return output
def format_explainer_response(
result: Any,
tone: float,
verbosity: float,
transparency: float,
) -> str:
if not result or not getattr(result, "understood", False):
return "I can help explain what the question is asking, but I need the full wording of the question."
output: List[str] = []
header = _explainer_header(tone)
if header:
output.append(header)
output.append("")
summary = _coerce_string(getattr(result, "summary", ""))
teaching_points = _coerce_list(getattr(result, "teaching_points", []))
if summary and not _is_wrapper_line(summary):
output.append(summary)
scaffold_sections = _build_scaffold_sections(result, verbosity, transparency)
if scaffold_sections:
output.extend(scaffold_sections)
if teaching_points and verbosity >= 0.5:
output.append("")
output.append("Key teaching points:")
limit = 2 if verbosity < 0.75 else 4
for item in teaching_points[:limit]:
output.append(f"- {item}")
plain = _coerce_string(getattr(result, "plain_english", ""))
if plain and not summary and not _is_wrapper_line(plain):
output.append(plain)
asks_for = _coerce_string(getattr(result, "asks_for", ""))
if asks_for and transparency >= 0.3:
output.append("")
output.append(f"The question is asking for: {asks_for}")
if verbosity >= 0.4:
_append_section(
output,
"What the question gives you:",
getattr(result, "givens", []) or [],
5 if verbosity >= 0.7 else 3,
)
if verbosity >= 0.65:
_append_section(
output,
"Constraints or conditions:",
getattr(result, "constraints", []) or [],
5,
)
if transparency >= 0.45:
_append_section(
output,
"Key relationship:",
getattr(result, "relationships", []) or [],
4,
)
if transparency >= 0.5:
_append_section(
output,
"Concepts you will probably need:",
getattr(result, "needed_concepts", []) or [],
4,
)
if transparency >= 0.55 or verbosity >= 0.7:
_append_section(
output,
"Watch out for:",
getattr(result, "trap_notes", []) or [],
4,
)
strategy_hint = _coerce_string(getattr(result, "strategy_hint", ""))
if strategy_hint and verbosity >= 0.35 and not _is_wrapper_line(strategy_hint):
output.append("")
output.append(f"Best starting move: {strategy_hint}")
if transparency >= 0.8:
topic = _explainer_topic(result)
why_seed_lines: List[str] = []
if summary:
why_seed_lines.append(summary)
scaffold = _get_scaffold(result)
if scaffold is not None:
ask = _coerce_string(getattr(scaffold, "ask", ""))
first_move = _coerce_string(getattr(scaffold, "first_move", ""))
if ask:
why_seed_lines.append(ask)
if first_move:
why_seed_lines.append(first_move)
if not why_seed_lines:
why_seed_lines.extend(teaching_points[:2])
if why_seed_lines:
output.append("")
output.append(_why_line(topic, why_seed_lines))
final_lines = []
previous_key = None
for line in output:
if line is None:
continue
text = str(line).rstrip()
key = _normalize_key(text)
if key == previous_key and key:
continue
final_lines.append(text)
previous_key = key
text = "\n".join(final_lines).strip()
if not text:
return "I can help explain what the question is asking, but I need the full wording of the question."
return text