File size: 10,337 Bytes
affe63a 020d9c6 affe63a 020d9c6 affe63a 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 affe63a ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 47fe089 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 47fe089 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 47fe089 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 ae81756 020d9c6 47fe089 8ff3d0f 020d9c6 8ff3d0f 020d9c6 8ff3d0f 020d9c6 8ff3d0f ae81756 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | 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),
)
|