GameAI / formatting.py
j-js's picture
Update formatting.py
020d9c6 verified
raw
history blame
11.1 kB
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)
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 _dedupe_lines(lines: List[str]) -> List[str]:
seen = set()
output: List[str] = []
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 _strip_bullet_prefix(text: str) -> str:
return re.sub(r"^\s*[-•]\s*", "", (text or "").strip())
def _normalize_display_lines(lines: List[str]) -> List[str]:
cleaned: List[str] = []
for line in lines:
item = _strip_bullet_prefix(line)
if item:
cleaned.append(item)
return cleaned
def _limit_steps(steps: List[str], verbosity: float, minimum: int = 1) -> List[str]:
if not steps:
return []
if verbosity < 0.2:
limit = minimum
elif verbosity < 0.45:
limit = max(minimum, 2)
elif verbosity < 0.7:
limit = max(minimum, 3)
else:
limit = max(minimum, 5)
return steps[:limit]
def _extract_topic_from_text(text: str, fallback: Optional[str] = None) -> str:
low = (text or "").lower()
if fallback:
return fallback
if any(word in low for word in ["equation", "variable", "isolate", "algebra"]):
return "algebra"
if any(word in low for word in ["percent", "percentage", "%"]):
return "percent"
if any(word in low for word in ["ratio", "proportion"]):
return "ratio"
if any(word in low for word in ["probability", "outcome", "chance", "odds"]):
return "probability"
if any(word in low for word in ["mean", "median", "average", "data", "variance", "standard deviation"]):
return "statistics"
if any(word in low for word in ["triangle", "circle", "angle", "area", "perimeter", "circumference", "rectangle"]):
return "geometry"
if any(word in low for word in ["integer", "factor", "multiple", "prime", "remainder", "divisible"]):
return "number_theory"
return "general"
def _why_line(topic: Optional[str]) -> str:
topic = (topic or "general").strip().lower()
if topic == "algebra":
return "Why this helps: algebra becomes easier when you reverse operations in order and keep the variable isolated."
if topic == "percent":
return "Why this helps: percent problems work best when you identify the base amount and apply each change to the correct value."
if topic == "ratio":
return "Why this helps: ratios represent parts of a whole, so turning them into total parts keeps the set-up consistent."
if topic == "probability":
return "Why this helps: probability is favorable outcomes over total outcomes, so clear counting matters first."
if topic == "statistics":
return "Why this helps: statistics questions depend on choosing the right measure before you calculate anything."
if topic == "geometry":
return "Why this helps: geometry usually becomes manageable once you identify the correct formula and substitute carefully."
if topic == "number_theory":
return "Why this helps: number theory questions usually depend on patterns, divisibility, or factor structure rather than brute force."
return "Why this helps: identifying the structure first makes the next step clearer and reduces avoidable mistakes."
def _tone_rewrite(line: str, tone: float, position: int = 0) -> str:
text = (line or "").strip()
if not text:
return text
if tone < 0.2:
return text
if tone < 0.45:
return text[:1].upper() + text[1:] if text else text
if tone < 0.75:
return f"Start here: {text[0].lower() + text[1:] if len(text) > 1 else text.lower()}" if position == 0 else text
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
def _transparency_expansion(line: str, topic: str, transparency: float, position: int = 0) -> str:
text = (line or "").strip()
if not text:
return text
if transparency < 0.35:
return text
if transparency < 0.7:
if position == 0:
if topic == "algebra":
return f"{text} This helps isolate the variable."
if topic == "percent":
return f"{text} This keeps track of the percent relationship correctly."
if topic == "ratio":
return f"{text} This helps turn the ratio into usable parts."
if topic == "probability":
return f"{text} This sets up favorable versus total outcomes."
return text
if position == 0:
if topic == "algebra":
return f"{text} In algebra, the goal is to isolate the variable by reversing operations in the opposite order."
if topic == "percent":
return f"{text} Percent questions are easiest when you identify the base amount and apply the change to the right quantity."
if topic == "ratio":
return f"{text} Ratio questions become easier once you treat the ratio numbers as parts of one total."
if topic == "probability":
return f"{text} Probability depends on counting the favorable cases and the total possible cases accurately."
if topic == "statistics":
return f"{text} Statistics questions usually depend on choosing the correct measure before calculating."
if topic == "geometry":
return f"{text} Geometry problems are usually solved by matching the shape to the correct formula first."
return f"{text} This works because it clarifies the structure 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.append(prefix)
output.append("")
if help_mode == "hint":
if verbosity < 0.25:
idx = max(0, min(int(hint_stage or 1) - 1, len(normalized_lines) - 1))
shown = [normalized_lines[idx]]
elif verbosity < 0.6:
idx = max(0, min(int(hint_stage or 1) - 1, len(normalized_lines) - 1))
shown = normalized_lines[idx:idx + 2] or [normalized_lines[idx]]
else:
shown = normalized_lines[: min(3, len(normalized_lines))]
shown = _styled_lines(shown, tone, transparency, resolved_topic)
output.append("Hint:")
for line in shown:
output.append(f"- {line}")
if transparency >= 0.75:
output.append("")
output.append(_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:")
for line in shown:
output.append(f"- {line}")
if transparency >= 0.7:
output.append("")
output.append(_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:")
for line in shown:
output.append(f"- {line}")
if transparency >= 0.6:
output.append("")
output.append(_why_line(resolved_topic))
return "\n".join(output).strip()
if help_mode == "answer":
shown = _limit_steps(normalized_lines, verbosity, minimum=2)
shown = _styled_lines(shown, tone, transparency if verbosity >= 0.45 else min(transparency, 0.4), resolved_topic)
output.append("Answer path:")
for line in shown:
output.append(f"- {line}")
if transparency >= 0.75:
output.append("")
output.append(_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)
for line in shown:
output.append(f"- {line}")
if transparency >= 0.8:
output.append("")
output.append(_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),
)