GameAI / formatting.py
j-js's picture
Upload 5 files
ae81756 verified
from __future__ import annotations
import re
from typing import Any, List, Optional
def style_prefix(tone: float) -> str:
if tone < 0.2:
return ""
if tone < 0.45:
return "Ok —"
if tone < 0.75:
return "Let’s work through it."
return "You’ve got this — let’s work through it step by step."
def _clean_lines(core: str) -> List[str]:
lines: List[str] = []
for line in (core or "").splitlines():
cleaned = line.strip()
if cleaned:
lines.append(cleaned.lstrip("- ").strip())
return lines
def _normalize_key(text: str) -> str:
return re.sub(r"\s+", " ", (text or "").strip().lower())
def _dedupe_lines(lines: List[str]) -> List[str]:
seen = set()
out: List[str] = []
for line in lines:
key = _normalize_key(line)
if key and key not in seen:
seen.add(key)
out.append(line.strip())
return out
def _extract_topic_from_text(core: str, topic: Optional[str]) -> str:
if topic:
return str(topic).strip().lower()
text = (core or "").lower()
if "probability" in text or "favorable" in text or "sample space" in text:
return "probability"
if "percent" in text or "%" in text:
return "percent"
if "ratio" in text or "multiplier" in text:
return "ratio"
if "variable" in text or "equation" in text:
return "algebra"
if "variability" in text or "standard deviation" in text or "spread" in text:
return "statistics"
if "rectangle" in text or "perimeter" in text or "area" in text:
return "geometry"
return "general"
def _normalize_display_lines(lines: List[str]) -> List[str]:
return [re.sub(r"\s+", " ", (line or "").strip()) for line in lines if str(line).strip()]
def _limit_steps(lines: List[str], verbosity: float, minimum: int = 1) -> List[str]:
if not lines:
return []
if verbosity < 0.22:
limit = minimum
elif verbosity < 0.55:
limit = max(minimum, min(2, len(lines)))
elif verbosity < 0.82:
limit = max(minimum, min(4, len(lines)))
else:
limit = len(lines)
return lines[:limit]
def _why_line(topic: str) -> str:
if topic == "algebra":
return "Why this helps: reversing operations in the right order keeps the equation equivalent while you isolate the variable."
if topic == "percent":
return "Why this helps: percent questions usually break when the base quantity is chosen incorrectly."
if topic == "ratio":
return "Why this helps: ratio numbers are usually parts, not the final quantities themselves."
if topic == "probability":
return "Why this helps: the numerator and denominator must be counted under the same rules."
if topic == "statistics":
return "Why this helps: statistics questions depend on choosing the right measure before calculating."
if topic == "geometry":
return "Why this helps: matching the right formula to the shape simplifies the rest of the work."
return "Why this helps: getting the structure right first makes the next step clearer."
def _tone_rewrite(line: str, tone: float, position: int = 0) -> str:
text = (line or "").strip()
if not text:
return text
if tone < 0.25:
return text
if tone < 0.55:
return f"Start here: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text
if tone < 0.8:
return f"A good place to start is this: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text
return f"You can start with this: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text
def _transparency_expansion(line: str, topic: str, transparency: float, position: int = 0) -> str:
text = (line or "").strip()
if not text or transparency < 0.35:
return text
if transparency < 0.7:
if position == 0:
if topic == "algebra":
return f"{text} This keeps the equation balanced while you isolate the variable."
if topic == "percent":
return f"{text} This keeps the percent relationship tied to the correct base quantity."
if topic == "ratio":
return f"{text} This turns the ratio into usable quantities instead of labels."
if topic == "probability":
return f"{text} This separates successful outcomes from total outcomes."
return text
if position == 0:
if topic == "algebra":
return f"{text} In algebra, each step should preserve an equivalent equation so the solution does not change while the variable is isolated."
if topic == "percent":
return f"{text} Percent problems become clearer once the base quantity is fixed, because every percentage must refer back to some amount."
if topic == "ratio":
return f"{text} Ratio numbers usually describe relative parts, so turning them into multiples of one common quantity is what makes the setup usable."
if topic == "probability":
return f"{text} Probability depends on a consistent sample space, so the numerator and denominator must be counted under the same rules."
if topic == "statistics":
return f"{text} Statistics questions often hinge on choosing the right measure first, because different measures capture different features of the data."
if topic == "geometry":
return f"{text} Geometry problems often become routine once the correct formula is chosen, because the rest is usually substitution and algebra."
return f"{text} This makes the underlying structure explicit before you calculate."
return text
def _styled_lines(lines: List[str], tone: float, transparency: float, topic: str) -> List[str]:
output: List[str] = []
for i, line in enumerate(lines):
rewritten = _tone_rewrite(line, tone, i)
rewritten = _transparency_expansion(rewritten, topic, transparency, i)
output.append(rewritten)
return output
def format_reply(
core: str,
tone: float,
verbosity: float,
transparency: float,
help_mode: str,
hint_stage: int = 0,
topic: Optional[str] = None,
) -> str:
prefix = style_prefix(tone)
core = (core or "").strip()
if not core:
return prefix or "Start with the structure of the problem."
lines = _dedupe_lines(_clean_lines(core))
if not lines:
return prefix or "Start with the structure of the problem."
resolved_topic = _extract_topic_from_text(core, topic)
normalized_lines = _normalize_display_lines(lines)
output: List[str] = []
if prefix:
output.extend([prefix, ""])
if help_mode == "hint":
idx = max(0, min(int(hint_stage or 1) - 1, len(normalized_lines) - 1))
if verbosity < 0.25:
shown = [normalized_lines[idx]]
elif verbosity < 0.62:
shown = normalized_lines[idx: idx + 2] or [normalized_lines[idx]]
else:
shown = normalized_lines[: min(4, len(normalized_lines))]
shown = _styled_lines(shown, tone, transparency, resolved_topic)
output.append("Hint:")
output.extend(f"- {line}" for line in shown)
if transparency >= 0.75:
output.extend(["", _why_line(resolved_topic)])
return "\n".join(output).strip()
if help_mode in {"walkthrough", "instruction", "step_by_step"}:
shown = _limit_steps(normalized_lines, verbosity, minimum=2 if help_mode == "walkthrough" else 1)
shown = _styled_lines(shown, tone, transparency, resolved_topic)
output.append("Walkthrough:" if help_mode == "walkthrough" else "Step-by-step path:")
output.extend(f"- {line}" for line in shown)
if transparency >= 0.7:
output.extend(["", _why_line(resolved_topic)])
return "\n".join(output).strip()
if help_mode in {"method", "explain", "concept", "definition"}:
shown = _limit_steps(normalized_lines, verbosity, minimum=1)
shown = _styled_lines(shown, tone, transparency, resolved_topic)
output.append("Explanation:")
output.extend(f"- {line}" for line in shown)
if transparency >= 0.6:
output.extend(["", _why_line(resolved_topic)])
return "\n".join(output).strip()
if help_mode == "answer":
shown = _limit_steps(normalized_lines, verbosity, minimum=2)
answer_transparency = transparency if verbosity >= 0.45 else min(transparency, 0.45)
shown = _styled_lines(shown, tone, answer_transparency, resolved_topic)
output.append("Answer path:")
output.extend(f"- {line}" for line in shown)
if transparency >= 0.75:
output.extend(["", _why_line(resolved_topic)])
return "\n".join(output).strip()
shown = _limit_steps(normalized_lines, verbosity, minimum=1)
shown = _styled_lines(shown, tone, transparency, resolved_topic)
output.extend(f"- {line}" for line in shown)
if transparency >= 0.8:
output.extend(["", _why_line(resolved_topic)])
return "\n".join(output).strip()
def format_explainer_response(
result: Any,
tone: float,
verbosity: float,
transparency: float,
help_mode: str = "explain",
hint_stage: int = 0,
) -> str:
if not result:
return "I can help explain what the question is asking, but I need the full wording of the question."
summary = getattr(result, "summary", "") or ""
teaching_points = getattr(result, "teaching_points", []) or []
core_lines: List[str] = []
if isinstance(summary, str) and summary.strip():
core_lines.append(summary.strip())
if isinstance(teaching_points, list):
for item in teaching_points:
text = str(item).strip()
if text:
core_lines.append(text)
if not core_lines:
core_lines = ["Start by identifying what the question is asking."]
return format_reply(
core="\n".join(core_lines),
tone=tone,
verbosity=verbosity,
transparency=transparency,
help_mode=help_mode,
hint_stage=hint_stage,
topic=getattr(result, "topic", None),
)